mirror of
https://gitee.com/dcren/initializr.git
synced 2026-02-26 05:32:58 +08:00
Introduce class name
Closes gh-1425
This commit is contained in:
committed by
Stephane Nicoll
parent
d22201b2d6
commit
1d9e6b5b7b
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -37,8 +37,8 @@ import org.springframework.util.ClassUtils;
|
|||||||
* that. Emit {@code "null"} if the value is {@code null}. Does not handle multi-line
|
* that. Emit {@code "null"} if the value is {@code null}. Does not handle multi-line
|
||||||
* strings.
|
* strings.
|
||||||
* <li>{@code $T} emits a type reference. Arguments for types may be plain
|
* <li>{@code $T} emits a type reference. Arguments for types may be plain
|
||||||
* {@linkplain Class classes}, fully qualified class names, and fully qualified
|
* {@linkplain Class classes}, {@linkplain ClassName class names}, fully qualified class
|
||||||
* functions.</li>
|
* names, and fully qualified functions.</li>
|
||||||
* <li>{@code $$} emits a dollar sign.
|
* <li>{@code $$} emits a dollar sign.
|
||||||
* <li>{@code $]} ends a statement and emits the configured
|
* <li>{@code $]} ends a statement and emits the configured
|
||||||
* {@linkplain FormattingOptions#statementSeparator() statement separator}.
|
* {@linkplain FormattingOptions#statementSeparator() statement separator}.
|
||||||
@@ -253,9 +253,13 @@ public final class CodeBlock {
|
|||||||
this.imports.add(type.getName());
|
this.imports.add(type.getName());
|
||||||
return type.getSimpleName();
|
return type.getSimpleName();
|
||||||
}
|
}
|
||||||
if (arg instanceof String className) {
|
if (arg instanceof ClassName className) {
|
||||||
this.imports.add(className);
|
this.imports.add(className.getName());
|
||||||
return ClassUtils.getShortName(className);
|
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));
|
throw new IllegalArgumentException("Failed to extract type from '%s'".formatted(arg));
|
||||||
}
|
}
|
||||||
|
|||||||
29
initializr-generator/src/test/java/com/example/Example.java
Normal file
29
initializr-generator/src/test/java/com/example/Example.java
Normal 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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -125,6 +125,13 @@ class CodeBlockTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void codeBlockWithTypePlaceholderAndClassNameAddsImport() {
|
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");
|
CodeBlock code = CodeBlock.of("return $T.truncate(myString)", "com.example.StringUtils");
|
||||||
assertThat(writeJava(code)).isEqualTo("return StringUtils.truncate(myString)");
|
assertThat(writeJava(code)).isEqualTo("return StringUtils.truncate(myString)");
|
||||||
assertThat(code.getImports()).containsExactly("com.example.StringUtils");
|
assertThat(code.getImports()).containsExactly("com.example.StringUtils");
|
||||||
|
|||||||
Reference in New Issue
Block a user