Skip to content

Commit a70b19b

Browse files
committed
Add support for Spring RestController classes
1 parent a087334 commit a70b19b

File tree

12 files changed

+704
-2
lines changed

12 files changed

+704
-2
lines changed

components/sbm-core/src/main/java/org/springframework/sbm/java/impl/OpenRewriteAnnotation.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ public String getFullyQualifiedName() {
7373
}
7474

7575
@Override
76-
public boolean hasAttribute(String timeout) {
77-
return false;
76+
public boolean hasAttribute(String attribute) {
77+
return getAttributes().containsKey(attribute);
7878
}
7979

8080
@Override

components/sbm-recipes-spring-cloud/pom.xml

+8
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@
4646
<groupId>org.springframework.boot</groupId>
4747
<artifactId>spring-boot-starter-test</artifactId>
4848
</dependency>
49+
<dependency>
50+
<groupId>org.springframework</groupId>
51+
<artifactId>spring-web</artifactId>
52+
</dependency>
53+
<dependency>
54+
<groupId>org.antlr</groupId>
55+
<artifactId>ST4</artifactId>
56+
</dependency>
4957
<dependency>
5058
<groupId>org.springframework.sbm</groupId>
5159
<artifactId>recipe-test-support</artifactId>

components/sbm-support-boot/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
<groupId>org.springframework.boot</groupId>
5555
<artifactId>spring-boot-starter-freemarker</artifactId>
5656
</dependency>
57+
<dependency>
58+
<groupId>org.springframework</groupId>
59+
<artifactId>spring-web</artifactId>
60+
</dependency>
5761
<dependency>
5862
<groupId>org.springframework.boot</groupId>
5963
<artifactId>spring-boot-starter-test</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2021 - 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.sbm.boot.web.api;
18+
19+
import org.springframework.sbm.java.api.*;
20+
21+
import java.util.List;
22+
import java.util.stream.Collectors;
23+
24+
/**
25+
* Provides an API for classes annotated with {@code RestController}.
26+
*
27+
* @author Fabian Krüger
28+
*/
29+
public record RestControllerBean(JavaSource js, Type restControllerType) {
30+
private static final RestMethodMapper restMethodMapper = new RestMethodMapper();
31+
public static final List<String> SPRING_REST_METHOD_ANNOTATIONS = SpringRestMethodAnnotation
32+
.getAll()
33+
.stream()
34+
.map(SpringRestMethodAnnotation::getFullyQualifiedName)
35+
.toList();
36+
37+
/**
38+
* Return the list of methods annotated with any of the Spring Boot request mapping annotations like
39+
* {@code @RequestMapping} or {@code @GetMapping}.
40+
*/
41+
public List<RestMethod> getRestMethods() {
42+
return this.restControllerType.getMethods().stream()
43+
.filter(this::isRestMethod)
44+
.map(Method.class::cast)
45+
.map(restMethodMapper::map)
46+
.collect(Collectors.toList());
47+
}
48+
49+
private boolean isRestMethod(Method method) {
50+
return method.getAnnotations().stream()
51+
.anyMatch(methodAnnotation -> SPRING_REST_METHOD_ANNOTATIONS.contains(methodAnnotation.getFullyQualifiedName()));
52+
}
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2021 - 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.sbm.boot.web.api;
18+
19+
import lombok.Builder;
20+
import org.springframework.sbm.java.api.Method;
21+
import org.springframework.sbm.java.api.MethodParam;
22+
import org.springframework.web.bind.annotation.RequestMethod;
23+
24+
import java.util.List;
25+
import java.util.Optional;
26+
27+
/**
28+
* Record of request mapping annotation data (e.g. {@code RequestMapping} or {@code GetMapping}).
29+
*
30+
* @param path the list of mapped paths
31+
* @param name
32+
* @param method
33+
* @param params
34+
* @param headers the list of headers
35+
* @param consumes the effective media types
36+
* @param produces the effective media types
37+
* @param methodReference the method
38+
* @param returnType the fully qualified of the return type
39+
* @param requestBodyParameterType the fully qualified name of parameter annotated with {@RequestBody}
40+
*
41+
* @author Fabian Krüger
42+
*/
43+
@Builder
44+
public record RestMethod(
45+
List<String> path,
46+
String name,
47+
List<RequestMethod> method,
48+
List<String> params,
49+
List<String> headers,
50+
List<String> consumes,
51+
List<String> produces,
52+
Method methodReference,
53+
String returnType,
54+
Optional<MethodParam> requestBodyParameterType) {
55+
56+
public Optional<MethodParam> requestBodyParameterType() {
57+
if(requestBodyParameterType == null) return Optional.empty();
58+
return requestBodyParameterType;
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright 2021 - 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.sbm.boot.web.api;
18+
19+
import org.jetbrains.annotations.NotNull;
20+
import org.openrewrite.java.tree.J;
21+
import org.openrewrite.java.tree.JavaType;
22+
import org.springframework.http.MediaType;
23+
import org.springframework.sbm.java.api.Annotation;
24+
import org.springframework.sbm.java.api.Expression;
25+
import org.springframework.sbm.java.api.Method;
26+
import org.springframework.sbm.java.impl.OpenRewriteExpression;
27+
import org.springframework.web.bind.annotation.RequestMethod;
28+
29+
import java.util.ArrayList;
30+
import java.util.List;
31+
import java.util.function.Consumer;
32+
import java.util.regex.Pattern;
33+
import java.util.stream.Collectors;
34+
35+
import static java.util.function.Predicate.not;
36+
import static org.springframework.sbm.boot.web.api.SpringRestMethodAnnotation.*;
37+
38+
class RestMethodMapper {
39+
40+
public static final List<String> SPRING_REST_METHOD_ANNOTATIONS = SpringRestMethodAnnotation.getAll().stream().map(SpringRestMethodAnnotation::getFullyQualifiedName)
41+
.collect(Collectors.toList());
42+
43+
public RestMethod map(Method method) {
44+
return mapToRestMethod(method);
45+
}
46+
47+
private RestMethod mapToRestMethod(Method method) {
48+
RestMethod.RestMethodBuilder builder = RestMethod.builder();
49+
Annotation annotation = getRestMethodAnnotation(method);
50+
builder.methodReference(method);
51+
buildRestMethod(method, annotation, builder);
52+
buildRequestBodyType(method, builder);
53+
return builder.build();
54+
}
55+
56+
private void buildRequestBodyType(Method method, RestMethod.RestMethodBuilder builder) {
57+
builder.requestBodyParameterType(method.getParams().stream()
58+
.filter(p -> p.containsAnnotation(Pattern.compile(".*\\.RequestBody")))
59+
.findFirst());
60+
}
61+
62+
private Annotation getRestMethodAnnotation(Method method) {
63+
return method.getAnnotations().stream()
64+
.filter(methodAnnotations -> SPRING_REST_METHOD_ANNOTATIONS.stream().anyMatch(fqName -> methodAnnotations.getFullyQualifiedName().equals(fqName)))
65+
.findFirst()
66+
.orElseThrow(() -> new IllegalArgumentException(String.format("The provided method '%s' has no Spring Request Mapping annpotation.", method.getName())));
67+
}
68+
69+
private void buildRestMethod(Method method, Annotation annotation, RestMethod.RestMethodBuilder builder) {
70+
String name = "name";
71+
if(annotation.hasAttribute(name)) {
72+
mapNameAttribute(annotation, builder, name);
73+
}
74+
extractExistingAnnotationAttribute(annotation, "value", values -> builder.path(values));
75+
extractExistingAnnotationAttribute(annotation, "path", paths -> builder.path(paths));
76+
extractExistingAnnotationAttribute(annotation, "params", params -> builder.params(params));
77+
extractExistingAnnotationAttribute(annotation, "consumes", consumes -> builder.consumes(consumes));
78+
extractExistingAnnotationAttribute(annotation, "produces", produces -> builder.produces(produces));
79+
extractExistingAnnotationAttribute(annotation, "headers", headers -> builder.headers(headers));
80+
mapMethodAttribute(annotation, builder);
81+
82+
if(!annotation.hasAttribute("consumes")) {
83+
builder.consumes(List.of(MediaType.APPLICATION_JSON_VALUE));
84+
}
85+
if(!annotation.hasAttribute("produces")) {
86+
builder.produces(List.of(MediaType.APPLICATION_JSON_VALUE));
87+
}
88+
89+
String fullyQualifiedReturnType = ((JavaType.Class) method
90+
.getMethodDecl()
91+
.getReturnTypeExpression()
92+
.getType()).getFullyQualifiedName();
93+
94+
builder.returnType(fullyQualifiedReturnType);
95+
}
96+
97+
private void extractExistingAnnotationAttribute(Annotation annotation, String attribute, Consumer<List<String>> valueConsumer) {
98+
if(annotation.hasAttribute(attribute)) {
99+
extractAnnotationAttribute(annotation, attribute, valueConsumer);
100+
}
101+
}
102+
103+
private void extractAnnotationAttribute(Annotation annotation, String attribute, Consumer<List<String>> consumer) {
104+
Expression expression = annotation.getAttributes().get(attribute);
105+
List<String> values = handleExpression(expression);
106+
consumer.accept(values);
107+
}
108+
109+
private void mapNameAttribute(Annotation annotation, RestMethod.RestMethodBuilder builder, String name) {
110+
Expression value = annotation.getAttributes().get(name);
111+
String s = value.getAssignmentRightSide().printVariable();
112+
builder.name(s);
113+
}
114+
115+
private void mapMethodAttribute(Annotation annotation, RestMethod.RestMethodBuilder builder) {
116+
if(annotation.hasAttribute("method")) {
117+
List<String> methods = handleExpression(annotation.getAttribute("method"));
118+
builder.method(methods.stream().map(m -> RequestMethod.valueOf(m)).collect(Collectors.toList()));
119+
}
120+
findAndMapMethodAttribute(annotation, builder, GET_MAPPING.getFullyQualifiedName(), RequestMethod.GET);
121+
findAndMapMethodAttribute(annotation, builder, POST_MAPPING.getFullyQualifiedName(), RequestMethod.POST);
122+
findAndMapMethodAttribute(annotation, builder, PUT_MAPPING.getFullyQualifiedName(), RequestMethod.PUT);
123+
findAndMapMethodAttribute(annotation, builder, DELETE_MAPPING.getFullyQualifiedName(), RequestMethod.DELETE);
124+
findAndMapMethodAttribute(annotation, builder, PATCH_MAPPING.getFullyQualifiedName(), RequestMethod.PATCH);
125+
}
126+
127+
private void findAndMapMethodAttribute(Annotation annotation, RestMethod.RestMethodBuilder builder, String requestNapping, RequestMethod requestMethod) {
128+
if(annotation.getFullyQualifiedName().equals(requestNapping)) {
129+
builder.method(List.of(requestMethod));
130+
}
131+
}
132+
133+
private List<String> handleLiteral(org.openrewrite.java.tree.Expression wrapped) {
134+
J.Literal literal = J.Literal.class.cast(wrapped);
135+
return List.of((String)literal.getValue());
136+
}
137+
138+
@NotNull
139+
private List<String> handleArray(org.openrewrite.java.tree.Expression expression) {
140+
J.NewArray array = J.NewArray.class.cast(expression);
141+
List<String> elements = array
142+
.getInitializer()
143+
.stream()
144+
.map(e -> this.handleExpression(e))
145+
.filter(not(List::isEmpty))
146+
.map(s -> s.get(0))
147+
.collect(Collectors.toList());
148+
return elements;
149+
}
150+
151+
private List<String> handleExpression(Expression value) {
152+
OpenRewriteExpression openRewriteExpression = OpenRewriteExpression.class.cast(value);
153+
org.openrewrite.java.tree.Expression wrapped = openRewriteExpression.getWrapped();
154+
return handleExpression(wrapped);
155+
}
156+
157+
private List<String> handleExpression(org.openrewrite.java.tree.Expression expression) {
158+
List<String> elements = new ArrayList<>();
159+
if(J.Literal.class.isInstance(expression)) {
160+
elements = handleLiteral(expression);
161+
}
162+
else if(J.NewArray.class.isInstance(expression)) {
163+
elements = handleArray(expression);
164+
}
165+
else if(J.Assignment.class.isInstance(expression)) {
166+
J.Assignment assignment = J.Assignment.class.cast(expression);
167+
org.openrewrite.java.tree.Expression assignment1 = assignment.getAssignment();
168+
if(J.NewArray.class.isInstance(assignment1)) {
169+
elements = handleArray(assignment1);
170+
}
171+
else if(J.FieldAccess.class.isInstance(assignment.getAssignment())) {
172+
elements = List.of(staticFieldReference(assignment.getAssignment()));
173+
}
174+
} else if(J.FieldAccess.class.isInstance(expression)) {
175+
elements = List.of(staticFieldReference(expression));
176+
}
177+
return elements;
178+
}
179+
180+
private String staticFieldReference(org.openrewrite.java.tree.Expression expression) {
181+
J.FieldAccess fieldAccess = J.FieldAccess.class.cast(expression);
182+
String fqName = ((JavaType.Class) fieldAccess.getTarget().getType()).getFullyQualifiedName();
183+
Class<?> aClass = null;
184+
String methodName = fieldAccess.getName().getSimpleName();
185+
try {
186+
aClass = Class.forName(fqName);
187+
Object o = aClass.getDeclaredField(methodName).get(null);
188+
return o.toString();
189+
} catch (NoSuchFieldException | ClassNotFoundException | IllegalAccessException e) {
190+
throw new RuntimeException(String.format("Exception while attempting to resolve the value for constant '%s.%s'", aClass, methodName));
191+
}
192+
}
193+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2021 - 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.sbm.boot.web.api;
18+
19+
import lombok.Getter;
20+
21+
import java.util.Arrays;
22+
import java.util.List;
23+
24+
enum SpringRestMethodAnnotation {
25+
DELETE_MAPPING("org.springframework.web.bind.annotation.DeleteMapping"),
26+
GET_MAPPING("org.springframework.web.bind.annotation.GetMapping"),
27+
POST_MAPPING("org.springframework.web.bind.annotation.PostMapping"),
28+
PUT_MAPPING("org.springframework.web.bind.annotation.PutMapping"),
29+
PATCH_MAPPING("org.springframework.web.bind.annotation.PatchMapping"),
30+
REQUEST_MAPPING("org.springframework.web.bind.annotation.RequestMapping");
31+
@Getter
32+
private final String fullyQualifiedName;
33+
34+
SpringRestMethodAnnotation(String fqn) {
35+
this.fullyQualifiedName = fqn;
36+
}
37+
38+
public static List<SpringRestMethodAnnotation> getAll() {
39+
return Arrays.asList(values());
40+
}
41+
}

0 commit comments

Comments
 (0)