From 16456342f5c55e691596c5577c72ed6dceef6ade Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 13 Apr 2022 18:21:39 -0700 Subject: [PATCH] Add support for generating method names Add `MethodNameGenerator` to support generation of method names. See gh-28414 --- .../aot/generate/MethodNameGenerator.java | 124 ++++++++++++++++++ .../generate/MethodNameGeneratorTests.java | 93 +++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/MethodNameGenerator.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/MethodNameGeneratorTests.java diff --git a/spring-core/src/main/java/org/springframework/aot/generate/MethodNameGenerator.java b/spring-core/src/main/java/org/springframework/aot/generate/MethodNameGenerator.java new file mode 100644 index 00000000000..533813e260c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/MethodNameGenerator.java @@ -0,0 +1,124 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Generates unique method names that can be used in ahead-of-time generated + * source code. This class is stateful so one instance should be used per + * generated type. + * + * @author Phillip Webb + * @since 6.0 + */ +public class MethodNameGenerator { + + private final Map sequenceGenerator = new ConcurrentHashMap<>(); + + + /** + * Create a new {@link MethodNameGenerator} instance without any reserved + * names. + */ + public MethodNameGenerator() { + } + + /** + * Create a new {@link MethodNameGenerator} instance with the specified + * reserved names. + * @param reservedNames the method names to reserve + */ + public MethodNameGenerator(String... reservedNames) { + this(List.of(reservedNames)); + } + + /** + * Create a new {@link MethodNameGenerator} instance with the specified + * reserved names. + * @param reservedNames the method names to reserve + */ + public MethodNameGenerator(Iterable reservedNames) { + Assert.notNull(reservedNames, "'reservedNames' must not be null"); + for (String reservedName : reservedNames) { + addSequence(StringUtils.uncapitalize(reservedName)); + } + } + + + /** + * Generate a new method name from the given parts. + * @param parts the parts used to build the name. + * @return the generated method name + */ + public String generateMethodName(Object... parts) { + String generatedName = join(parts); + return addSequence(generatedName.isEmpty() ? "$$aot" : generatedName); + } + + private String addSequence(String name) { + int sequence = this.sequenceGenerator + .computeIfAbsent(name, key -> new AtomicInteger()).getAndIncrement(); + return (sequence > 0) ? name + sequence : name; + } + + /** + * Join the specified parts to create a valid camel case method name. + * @param parts the parts to join + * @return a method name from the joined parts. + */ + public static String join(Object... parts) { + Stream capitalizedPartNames = Arrays.stream(parts) + .map(MethodNameGenerator::getPartName).map(StringUtils::capitalize); + return StringUtils + .uncapitalize(capitalizedPartNames.collect(Collectors.joining())); + } + + private static String getPartName(@Nullable Object part) { + if (part == null) { + return ""; + } + if (part instanceof Class clazz) { + return clean(ClassUtils.getShortName(clazz)); + } + return clean(part.toString()); + } + + private static String clean(String string) { + char[] chars = string.toCharArray(); + StringBuilder name = new StringBuilder(chars.length); + boolean uppercase = false; + for (char ch : chars) { + char outputChar = (!uppercase) ? ch : Character.toUpperCase(ch); + name.append((!Character.isLetter(ch)) ? "" : outputChar); + uppercase = ch == '.'; + } + return name.toString(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/MethodNameGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/generate/MethodNameGeneratorTests.java new file mode 100644 index 00000000000..997853b4bd6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/MethodNameGeneratorTests.java @@ -0,0 +1,93 @@ +/* + * 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.io.InputStream; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MethodNameGenerator}. + * + * @author Phillip Webb + */ +class MethodNameGeneratorTests { + + private final MethodNameGenerator generator = new MethodNameGenerator(); + + @Test + void createWithReservedNamesReservesNames() { + MethodNameGenerator generator = new MethodNameGenerator("testName"); + assertThat(generator.generateMethodName("test", "name")).hasToString("testName1"); + } + + @Test + void generateMethodNameGeneratesName() { + String generated = this.generator.generateMethodName("register", "myBean", + "bean"); + assertThat(generated).isEqualTo("registerMyBeanBean"); + } + + @Test + void generateMethodNameWhenHasNonLettersGeneratesName() { + String generated = this.generator.generateMethodName("register", "myBean123", + "bean"); + assertThat(generated).isEqualTo("registerMyBeanBean"); + } + + @Test + void generateMethodNameWhenHasDotsGeneratesCamelCaseName() { + String generated = this.generator.generateMethodName("register", + "org.springframework.example.bean"); + assertThat(generated).isEqualTo("registerOrgSpringframeworkExampleBean"); + } + + @Test + void generateMethodNameWhenMultipleCallsGeneratesSequencedName() { + String generated1 = this.generator.generateMethodName("register", "myBean123", + "bean"); + String generated2 = this.generator.generateMethodName("register", "myBean!", + "bean"); + String generated3 = this.generator.generateMethodName("register", "myBean%%", + "bean"); + assertThat(generated1).isEqualTo("registerMyBeanBean"); + assertThat(generated2).isEqualTo("registerMyBeanBean1"); + assertThat(generated3).isEqualTo("registerMyBeanBean2"); + } + + @Test + void generateMethodNameWhenAllEmptyPartsGeneratesSetName() { + String generated = this.generator.generateMethodName("123"); + assertThat(generated).isEqualTo("$$aot"); + } + + @Test + void joinReturnsJoinedName() { + assertThat(MethodNameGenerator.join("get", "bean", "factory")) + .isEqualTo("getBeanFactory"); + assertThat(MethodNameGenerator.join("get", null, "factory")) + .isEqualTo("getFactory"); + assertThat(MethodNameGenerator.join(null, null)).isEqualTo(""); + assertThat(MethodNameGenerator.join("", null)).isEqualTo(""); + assertThat(MethodNameGenerator.join("get", InputStream.class)) + .isEqualTo("getInputStream"); + + } + +}