1
0
mirror of https://gitee.com/dcren/initializr.git synced 2025-04-05 17:38:06 +08:00

Introduce class name

Closes gh-1425
This commit is contained in:
Stephane Nicoll 2023-06-09 10:38:13 +02:00 committed by Stephane Nicoll
parent d22201b2d6
commit 1d9e6b5b7b
5 changed files with 414 additions and 5 deletions
initializr-generator/src
main/java/io/spring/initializr/generator/language
test/java
com/example
io/spring/initializr/generator/language

View File

@ -0,0 +1,200 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.generator.language;
import java.util.List;
import java.util.Objects;
import javax.lang.model.SourceVersion;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Type reference abstraction to refer to a {@link Class} that is not available on the
* classpath.
*
* @author Stephane Nicoll
*/
public final class ClassName {
private static final List<String> PRIMITIVE_NAMES = List.of("boolean", "byte", "short", "int", "long", "char",
"float", "double", "void");
private final String packageName;
private final String simpleName;
private final ClassName enclosingType;
private String canonicalName;
private ClassName(String packageName, String simpleName, ClassName enclosingType) {
this.packageName = packageName;
this.simpleName = simpleName;
this.enclosingType = enclosingType;
}
/**
* Create a {@link ClassName} based on the specified fully qualified name. The format
* of the class name must follow {@linkplain Class#getName()}, in particular inner
* classes should be separated by a {@code $}.
* @param fqName the fully qualified name of the class
* @return a class name
*/
public static ClassName of(String fqName) {
Assert.notNull(fqName, "'className' must not be null");
if (!isValidClassName(fqName)) {
throw new IllegalStateException("Invalid class name '" + fqName + "'");
}
if (!fqName.contains("$")) {
return createClassName(fqName);
}
String[] elements = fqName.split("(?<!\\$)\\$(?!\\$)");
ClassName className = createClassName(elements[0]);
for (int i = 1; i < elements.length; i++) {
className = new ClassName(className.getPackageName(), elements[i], className);
}
return className;
}
/**
* Create a {@link ClassName} based on the specified {@link Class}.
* @param type the class to wrap
* @return a class name
*/
public static ClassName of(Class<?> type) {
return of(type.getName());
}
/**
* Return the fully qualified name.
* @return the reflection target name
*/
public String getName() {
ClassName enclosingType = getEnclosingType();
String simpleName = getSimpleName();
return (enclosingType != null) ? (enclosingType.getName() + '$' + simpleName)
: addPackageIfNecessary(simpleName);
}
/**
* Return the package name.
* @return the package name
*/
public String getPackageName() {
return this.packageName;
}
/**
* Return the {@linkplain Class#getSimpleName() simple name}.
* @return the simple name
*/
public String getSimpleName() {
return this.simpleName;
}
/**
* Return the enclosing class name, or {@code null} if this instance does not have an
* enclosing type.
* @return the enclosing type, if any
*/
public ClassName getEnclosingType() {
return this.enclosingType;
}
/**
* Return the {@linkplain Class#getCanonicalName() canonical name}.
* @return the canonical name
*/
public String getCanonicalName() {
if (this.canonicalName == null) {
StringBuilder names = new StringBuilder();
buildName(this, names);
this.canonicalName = addPackageIfNecessary(names.toString());
}
return this.canonicalName;
}
private boolean isPrimitive() {
return isPrimitive(getSimpleName());
}
private static boolean isPrimitive(String name) {
return PRIMITIVE_NAMES.stream().anyMatch(name::startsWith);
}
private String addPackageIfNecessary(String part) {
if (this.packageName.isEmpty() || this.packageName.equals("java.lang") && isPrimitive()) {
return part;
}
return this.packageName + '.' + part;
}
private static boolean isValidClassName(String className) {
for (String s : className.split("\\.", -1)) {
String candidate = s.replace("[", "").replace("]", "");
if (!SourceVersion.isIdentifier(candidate)) {
return false;
}
}
return true;
}
private static ClassName createClassName(String className) {
int i = className.lastIndexOf('.');
if (i != -1) {
return new ClassName(className.substring(0, i), className.substring(i + 1), null);
}
else {
String packageName = (isPrimitive(className)) ? "java.lang" : "";
return new ClassName(packageName, className, null);
}
}
private static void buildName(ClassName className, StringBuilder sb) {
if (className == null) {
return;
}
String typeName = (className.getEnclosingType() != null) ? "." + className.getSimpleName()
: className.getSimpleName();
sb.insert(0, typeName);
buildName(className.getEnclosingType(), sb);
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ClassName className)) {
return false;
}
return getCanonicalName().equals(className.getCanonicalName());
}
@Override
public int hashCode() {
return Objects.hash(getCanonicalName());
}
@Override
public String toString() {
return getCanonicalName();
}
}

View File

@ -37,8 +37,8 @@ import org.springframework.util.ClassUtils;
* that. Emit {@code "null"} if the value is {@code null}. Does not handle multi-line
* strings.
* <li>{@code $T} emits a type reference. Arguments for types may be plain
* {@linkplain Class classes}, fully qualified class names, and fully qualified
* functions.</li>
* {@linkplain Class classes}, {@linkplain ClassName class names}, fully qualified class
* names, and fully qualified functions.</li>
* <li>{@code $$} emits a dollar sign.
* <li>{@code $]} ends a statement and emits the configured
* {@linkplain FormattingOptions#statementSeparator() statement separator}.
@ -253,9 +253,13 @@ public final class CodeBlock {
this.imports.add(type.getName());
return type.getSimpleName();
}
if (arg instanceof String className) {
this.imports.add(className);
return ClassUtils.getShortName(className);
if (arg instanceof ClassName className) {
this.imports.add(className.getName());
return className.getSimpleName();
}
if (arg instanceof String fqName) {
this.imports.add(fqName);
return ClassUtils.getShortName(fqName);
}
throw new IllegalArgumentException("Failed to extract type from '%s'".formatted(arg));
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example;
public class Example {
public static class Inner {
public static class Nested {
}
}
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.initializr.generator.language;
import java.util.stream.Stream;
import com.example.Example;
import com.example.Example.Inner;
import com.example.Example.Inner.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ClassName}.
*
* @author Stephane Nicoll
*/
class ClassNameTests {
@Test
void classNameWithTopLevelClassName() {
classNameWithTopLevelClass(ClassName.of("com.example.Example"));
}
@Test
void classNameWithTopLevelClass() {
classNameWithTopLevelClass(ClassName.of(Example.class));
}
private void classNameWithTopLevelClass(ClassName className) {
assertThat(className.getName()).isEqualTo("com.example.Example");
assertThat(className.getCanonicalName()).isEqualTo("com.example.Example");
assertThat(className.getPackageName()).isEqualTo("com.example");
assertThat(className.getSimpleName()).isEqualTo("Example");
assertThat(className.getEnclosingType()).isNull();
}
@Test
void classNameWithInnerClassName() {
classNameWithInnerClass(ClassName.of("com.example.Example$Inner"));
}
@Test
void classNameWithInnerClass() {
classNameWithInnerClass(ClassName.of(Inner.class));
}
private void classNameWithInnerClass(ClassName className) {
assertThat(className.getName()).isEqualTo("com.example.Example$Inner");
assertThat(className.getCanonicalName()).isEqualTo("com.example.Example.Inner");
assertThat(className.getPackageName()).isEqualTo("com.example");
assertThat(className.getSimpleName()).isEqualTo("Inner");
assertThat(className.getEnclosingType()).satisfies((enclosingType) -> {
assertThat(enclosingType.getCanonicalName()).isEqualTo("com.example.Example");
assertThat(enclosingType.getPackageName()).isEqualTo("com.example");
assertThat(enclosingType.getSimpleName()).isEqualTo("Example");
assertThat(enclosingType.getEnclosingType()).isNull();
});
}
@Test
void classNameWithNestedInnerClassName() {
classNameWithNestedInnerClass(ClassName.of("com.example.Example$Inner$Nested"));
}
@Test
void classNameWithNestedInnerClass() {
classNameWithNestedInnerClass(ClassName.of(Nested.class));
}
private void classNameWithNestedInnerClass(ClassName className) {
assertThat(className.getName()).isEqualTo("com.example.Example$Inner$Nested");
assertThat(className.getCanonicalName()).isEqualTo("com.example.Example.Inner.Nested");
assertThat(className.getPackageName()).isEqualTo("com.example");
assertThat(className.getSimpleName()).isEqualTo("Nested");
assertThat(className.getEnclosingType()).satisfies((enclosingType) -> {
assertThat(enclosingType.getCanonicalName()).isEqualTo("com.example.Example.Inner");
assertThat(enclosingType.getPackageName()).isEqualTo("com.example");
assertThat(enclosingType.getSimpleName()).isEqualTo("Inner");
assertThat(enclosingType.getEnclosingType()).satisfies((parentEnclosingType) -> {
assertThat(parentEnclosingType.getCanonicalName()).isEqualTo("com.example.Example");
assertThat(parentEnclosingType.getPackageName()).isEqualTo("com.example");
assertThat(parentEnclosingType.getSimpleName()).isEqualTo("Example");
assertThat(parentEnclosingType.getEnclosingType()).isNull();
});
});
}
@ParameterizedTest
@MethodSource("primitivesAndPrimitivesArray")
void primitivesAreHandledProperly(ClassName className, String expectedName) {
assertThat(className.getName()).isEqualTo(expectedName);
assertThat(className.getCanonicalName()).isEqualTo(expectedName);
assertThat(className.getPackageName()).isEqualTo("java.lang");
}
static Stream<Arguments> primitivesAndPrimitivesArray() {
return Stream.of(Arguments.of(ClassName.of("boolean"), "boolean"), Arguments.of(ClassName.of("byte"), "byte"),
Arguments.of(ClassName.of("short"), "short"), Arguments.of(ClassName.of("int"), "int"),
Arguments.of(ClassName.of("long"), "long"), Arguments.of(ClassName.of("char"), "char"),
Arguments.of(ClassName.of("float"), "float"), Arguments.of(ClassName.of("double"), "double"),
Arguments.of(ClassName.of("boolean[]"), "boolean[]"), Arguments.of(ClassName.of("byte[]"), "byte[]"),
Arguments.of(ClassName.of("short[]"), "short[]"), Arguments.of(ClassName.of("int[]"), "int[]"),
Arguments.of(ClassName.of("long[]"), "long[]"), Arguments.of(ClassName.of("char[]"), "char[]"),
Arguments.of(ClassName.of("float[]"), "float[]"), Arguments.of(ClassName.of("double[]"), "double[]"));
}
@ParameterizedTest
@MethodSource("arrays")
void arraysHaveSuitableReflectionTargetName(ClassName typeReference, String expectedName) {
assertThat(typeReference.getName()).isEqualTo(expectedName);
}
static Stream<Arguments> arrays() {
return Stream.of(Arguments.of(ClassName.of("java.lang.Object[]"), "java.lang.Object[]"),
Arguments.of(ClassName.of("java.lang.Integer[]"), "java.lang.Integer[]"),
Arguments.of(ClassName.of("com.example.Test[]"), "com.example.Test[]"));
}
@Test
void classNameInRootPackage() {
ClassName type = ClassName.of("MyRootClass");
assertThat(type.getCanonicalName()).isEqualTo("MyRootClass");
assertThat(type.getPackageName()).isEmpty();
}
@ParameterizedTest(name = "{0}")
@ValueSource(strings = { "com.example.Tes(t", "com.example..Test" })
void classNameWithInvalidClassName(String invalidClassName) {
assertThatIllegalStateException().isThrownBy(() -> ClassName.of(invalidClassName))
.withMessageContaining("Invalid class name");
}
@Test
void equalsWithIdenticalNameIsTrue() {
assertThat(ClassName.of(String.class)).isEqualTo(ClassName.of("java.lang.String"));
}
@Test
void equalsWithNonClassNameIsFalse() {
assertThat(ClassName.of(String.class)).isNotEqualTo("java.lang.String");
}
@Test
void toStringUsesCanonicalName() {
assertThat(ClassName.of(String.class)).hasToString("java.lang.String");
}
}

View File

@ -125,6 +125,13 @@ class CodeBlockTests {
@Test
void codeBlockWithTypePlaceholderAndClassNameAddsImport() {
CodeBlock code = CodeBlock.of("return $T.truncate(myString)", ClassName.of(StringUtils.class));
assertThat(writeJava(code)).isEqualTo("return StringUtils.truncate(myString)");
assertThat(code.getImports()).containsExactly(StringUtils.class.getName());
}
@Test
void codeBlockWithTypePlaceholderAndFullyQualifiedClassNameAddsImport() {
CodeBlock code = CodeBlock.of("return $T.truncate(myString)", "com.example.StringUtils");
assertThat(writeJava(code)).isEqualTo("return StringUtils.truncate(myString)");
assertThat(code.getImports()).containsExactly("com.example.StringUtils");