Skip to content

Commit fd191d1

Browse files
committed
Add GeneratedType infrastructure
This commit adds an infrastructure for code that generate types with the need to write to another package if privileged access is required. An abstraction around types where methods can be easily added is also available as part of this commit. Closes gh-28149
1 parent 14b147c commit fd191d1

File tree

5 files changed

+498
-0
lines changed

5 files changed

+498
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.generator;
18+
19+
import java.util.LinkedHashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.function.Function;
23+
import java.util.stream.Collectors;
24+
25+
import org.springframework.aot.hint.RuntimeHints;
26+
import org.springframework.javapoet.JavaFile;
27+
28+
/**
29+
* Default {@link GeneratedTypeContext} implementation.
30+
*
31+
* @author Stephane Nicoll
32+
* @since 6.0
33+
*/
34+
public class DefaultGeneratedTypeContext implements GeneratedTypeContext {
35+
36+
private final String packageName;
37+
38+
private final RuntimeHints runtimeHints;
39+
40+
private final Function<String, GeneratedType> generatedTypeFactory;
41+
42+
private final Map<String, GeneratedType> generatedTypes;
43+
44+
/**
45+
* Create a context targeting the specified package name and using the specified
46+
* factory to create a {@link GeneratedType} per requested package name.
47+
* @param packageName the main package name
48+
* @param generatedTypeFactory the factory to use to create a {@link GeneratedType}
49+
* based on a package name.
50+
*/
51+
public DefaultGeneratedTypeContext(String packageName, Function<String, GeneratedType> generatedTypeFactory) {
52+
this.packageName = packageName;
53+
this.runtimeHints = new RuntimeHints();
54+
this.generatedTypeFactory = generatedTypeFactory;
55+
this.generatedTypes = new LinkedHashMap<>();
56+
}
57+
58+
@Override
59+
public RuntimeHints runtimeHints() {
60+
return this.runtimeHints;
61+
}
62+
63+
@Override
64+
public GeneratedType getGeneratedType(String packageName) {
65+
return this.generatedTypes.computeIfAbsent(packageName, this.generatedTypeFactory);
66+
}
67+
68+
@Override
69+
public GeneratedType getMainGeneratedType() {
70+
return getGeneratedType(this.packageName);
71+
}
72+
73+
/**
74+
* Specify if a {@link GeneratedType} for the specified package name is registered.
75+
* @param packageName the package name to use
76+
* @return {@code true} if a type is registered for that package
77+
*/
78+
public boolean hasGeneratedType(String packageName) {
79+
return this.generatedTypes.containsKey(packageName);
80+
}
81+
82+
/**
83+
* Return the list of {@link JavaFile} of known generated type.
84+
* @return the java files of bootstrap classes in this instance
85+
*/
86+
public List<JavaFile> toJavaFiles() {
87+
return this.generatedTypes.values().stream()
88+
.map(GeneratedType::toJavaFile)
89+
.collect(Collectors.toList());
90+
}
91+
92+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.generator;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.function.Consumer;
22+
import java.util.function.Predicate;
23+
24+
import javax.lang.model.element.Modifier;
25+
26+
import org.springframework.javapoet.ClassName;
27+
import org.springframework.javapoet.JavaFile;
28+
import org.springframework.javapoet.MethodSpec;
29+
import org.springframework.javapoet.TypeSpec;
30+
31+
/**
32+
* Wrapper for a generated {@linkplain TypeSpec type}.
33+
*
34+
* @author Stephane Nicoll
35+
* @since 6.0
36+
*/
37+
public class GeneratedType {
38+
39+
private final ClassName className;
40+
41+
private final TypeSpec.Builder type;
42+
43+
private final List<MethodSpec> methods;
44+
45+
GeneratedType(ClassName className, Consumer<TypeSpec.Builder> type) {
46+
this.className = className;
47+
this.type = TypeSpec.classBuilder(className);
48+
type.accept(this.type);
49+
this.methods = new ArrayList<>();
50+
}
51+
52+
/**
53+
* Create an instance for the specified {@link ClassName}, customizing the type with
54+
* the specified {@link Consumer consumer callback}.
55+
* @param className the class name
56+
* @param type a callback to customize the type, i.e. to change default modifiers
57+
* @return a new {@link GeneratedType}
58+
*/
59+
public static GeneratedType of(ClassName className, Consumer<TypeSpec.Builder> type) {
60+
return new GeneratedType(className, type);
61+
}
62+
63+
/**
64+
* Create an instance for the specified {@link ClassName}, as a {@code public} type.
65+
* @param className the class name
66+
* @return a new {@link GeneratedType}
67+
*/
68+
public static GeneratedType of(ClassName className) {
69+
return of(className, type -> type.addModifiers(Modifier.PUBLIC));
70+
}
71+
72+
/**
73+
* Return the {@link ClassName} of this instance.
74+
* @return the class name
75+
*/
76+
public ClassName getClassName() {
77+
return this.className;
78+
}
79+
80+
/**
81+
* Customize the type of this instance.
82+
* @param type the consumer of the type builder
83+
* @return this for method chaining
84+
*/
85+
public GeneratedType customizeType(Consumer<TypeSpec.Builder> type) {
86+
type.accept(this.type);
87+
return this;
88+
}
89+
90+
/**
91+
* Add a method using the state of the specified {@link MethodSpec.Builder},
92+
* updating the name of the method if a similar method already exists.
93+
* @param method a method builder representing the method to add
94+
* @return the added method
95+
*/
96+
public MethodSpec addMethod(MethodSpec.Builder method) {
97+
MethodSpec methodToAdd = createUniqueNameIfNecessary(method.build());
98+
this.methods.add(methodToAdd);
99+
return methodToAdd;
100+
}
101+
102+
/**
103+
* Return a {@link JavaFile} with the state of this instance.
104+
* @return a java file
105+
*/
106+
public JavaFile toJavaFile() {
107+
return JavaFile.builder(this.className.packageName(),
108+
this.type.addMethods(this.methods).build()).indent("\t").build();
109+
}
110+
111+
private MethodSpec createUniqueNameIfNecessary(MethodSpec method) {
112+
List<MethodSpec> candidates = this.methods.stream().filter(isSimilar(method)).toList();
113+
if (candidates.isEmpty()) {
114+
return method;
115+
}
116+
MethodSpec updatedMethod = method.toBuilder().setName(method.name + "_").build();
117+
return createUniqueNameIfNecessary(updatedMethod);
118+
}
119+
120+
private Predicate<MethodSpec> isSimilar(MethodSpec method) {
121+
return candidate -> method.name.equals(candidate.name)
122+
&& method.parameters.size() == candidate.parameters.size();
123+
}
124+
125+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.generator;
18+
19+
import org.springframework.aot.hint.RuntimeHints;
20+
21+
/**
22+
* Context passed to object that can generate code, giving access to a main
23+
* {@link GeneratedType} as well as to a {@link GeneratedType} in a given
24+
* package if privileged access is required.
25+
*
26+
* @author Stephane Nicoll
27+
* @since 6.0
28+
*/
29+
public interface GeneratedTypeContext {
30+
31+
/**
32+
* Return the {@link RuntimeHints} instance to use to contribute hints for
33+
* generated types.
34+
* @return the runtime hints
35+
*/
36+
RuntimeHints runtimeHints();
37+
38+
/**
39+
* Return a {@link GeneratedType} for the specified package. If it does not
40+
* exist, it is created.
41+
* @param packageName the package name to use
42+
* @return a generated type
43+
*/
44+
GeneratedType getGeneratedType(String packageName);
45+
46+
/**
47+
* Return the main {@link GeneratedType}.
48+
* @return the generated type for the target package
49+
*/
50+
GeneratedType getMainGeneratedType();
51+
52+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.generator;
18+
19+
import javax.lang.model.element.Modifier;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.javapoet.ClassName;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Tests for {@link DefaultGeneratedTypeContext}.
29+
*
30+
* @author Stephane Nicoll
31+
*/
32+
class DefaultGeneratedTypeContextTests {
33+
34+
@Test
35+
void runtimeHints() {
36+
DefaultGeneratedTypeContext context = createComAcmeContext();
37+
assertThat(context.runtimeHints()).isNotNull();
38+
}
39+
40+
@Test
41+
void getGeneratedTypeMatchesGetMainGeneratedTypeForMainPackage() {
42+
DefaultGeneratedTypeContext context = createComAcmeContext();
43+
assertThat(context.getMainGeneratedType().getClassName()).isEqualTo(ClassName.get("com.acme", "Main"));
44+
assertThat(context.getGeneratedType("com.acme")).isSameAs(context.getMainGeneratedType());
45+
}
46+
47+
@Test
48+
void getMainGeneratedTypeIsLazilyCreated() {
49+
DefaultGeneratedTypeContext context = createComAcmeContext();
50+
assertThat(context.hasGeneratedType("com.acme")).isFalse();
51+
context.getMainGeneratedType();
52+
assertThat(context.hasGeneratedType("com.acme")).isTrue();
53+
}
54+
55+
@Test
56+
void getGeneratedTypeRegisterInstance() {
57+
DefaultGeneratedTypeContext context = createComAcmeContext();
58+
assertThat(context.hasGeneratedType("com.example")).isFalse();
59+
GeneratedType generatedType = context.getGeneratedType("com.example");
60+
assertThat(generatedType).isNotNull();
61+
assertThat(generatedType.getClassName().simpleName()).isEqualTo("Main");
62+
assertThat(context.hasGeneratedType("com.example")).isTrue();
63+
}
64+
65+
@Test
66+
void getGeneratedTypeReuseInstance() {
67+
DefaultGeneratedTypeContext context = createComAcmeContext();
68+
GeneratedType generatedType = context.getGeneratedType("com.example");
69+
assertThat(generatedType.getClassName().packageName()).isEqualTo("com.example");
70+
assertThat(context.getGeneratedType("com.example")).isSameAs(generatedType);
71+
}
72+
73+
@Test
74+
void toJavaFilesWithNoTypeIsEmpty() {
75+
DefaultGeneratedTypeContext writerContext = createComAcmeContext();
76+
assertThat(writerContext.toJavaFiles()).hasSize(0);
77+
}
78+
79+
@Test
80+
void toJavaFilesWithDefaultTypeIsAddedLazily() {
81+
DefaultGeneratedTypeContext writerContext = createComAcmeContext();
82+
writerContext.getMainGeneratedType();
83+
assertThat(writerContext.toJavaFiles()).hasSize(1);
84+
}
85+
86+
@Test
87+
void toJavaFilesWithDefaultTypeAndAdditionaTypes() {
88+
DefaultGeneratedTypeContext writerContext = createComAcmeContext();
89+
writerContext.getGeneratedType("com.example");
90+
writerContext.getGeneratedType("com.another");
91+
writerContext.getGeneratedType("com.another.another");
92+
assertThat(writerContext.toJavaFiles()).hasSize(3);
93+
}
94+
95+
private DefaultGeneratedTypeContext createComAcmeContext() {
96+
return new DefaultGeneratedTypeContext("com.acme", packageName ->
97+
GeneratedType.of(ClassName.get(packageName, "Main"), type -> type.addModifiers(Modifier.PUBLIC)));
98+
}
99+
100+
}

0 commit comments

Comments
 (0)