Skip to content

Commit 3a094ea

Browse files
authored
Flatten configuration class if possible (#287)
* Flatten configuration class if possible * Adjust bean method names. Indentation for test code snippets
1 parent e9f1079 commit 3a094ea

File tree

2 files changed

+368
-25
lines changed

2 files changed

+368
-25
lines changed

src/main/java/org/openrewrite/java/spring/boot2/WebSecurityConfigurerAdapter.java

Lines changed: 171 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,14 @@
2222
import org.openrewrite.java.JavaParser;
2323
import org.openrewrite.java.JavaTemplate;
2424
import org.openrewrite.java.MethodMatcher;
25+
import org.openrewrite.java.format.AutoFormatVisitor;
2526
import org.openrewrite.java.search.UsesType;
26-
import org.openrewrite.java.tree.J;
27-
import org.openrewrite.java.tree.JavaType;
28-
import org.openrewrite.java.tree.Space;
29-
import org.openrewrite.java.tree.TypeUtils;
27+
import org.openrewrite.java.tree.*;
3028
import org.openrewrite.marker.Markers;
3129
import org.openrewrite.marker.SearchResult;
3230

33-
import java.util.Arrays;
34-
import java.util.Collection;
35-
import java.util.Collections;
36-
import java.util.Comparator;
31+
import java.util.*;
32+
import java.util.stream.Collectors;
3733

3834
/**
3935
* @author Alex Boyko
@@ -67,6 +63,8 @@ public class WebSecurityConfigurerAdapter extends Recipe {
6763

6864
private static final String HAS_CONFLICT = "has-conflict";
6965

66+
private static final String FLATTEN_CLASSES = "flatten-classes";
67+
7068
@Override
7169
public String getDisplayName() {
7270
return "Spring Security 5.4 introduces the ability to configure HttpSecurity by creating a SecurityFilterChain bean";
@@ -87,21 +85,129 @@ protected TreeVisitor<?, ExecutionContext> getVisitor() {
8785
return new JavaIsoVisitor<ExecutionContext>() {
8886
@Override
8987
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext context) {
90-
if (TypeUtils.isAssignableTo(FQN_WEB_SECURITY_CONFIGURER_ADAPTER, classDecl.getType())
91-
&& classDecl.getLeadingAnnotations().stream().anyMatch(a -> TypeUtils.isOfClassType(a.getType(), FQN_CONFIGURATION))) {
92-
boolean hasConflict = false;
88+
boolean isWebSecurityConfigurerAdapterClass = TypeUtils.isAssignableTo(FQN_WEB_SECURITY_CONFIGURER_ADAPTER, classDecl.getType())
89+
&& isAnnotatedWith(classDecl.getLeadingAnnotations(), FQN_CONFIGURATION);
90+
boolean hasConflict = false;
91+
if (isWebSecurityConfigurerAdapterClass) {
9392
for (JavaType.Method method : classDecl.getType().getMethods()) {
9493
if (isConflictingMethod(method, method.getName())) {
9594
hasConflict = true;
95+
break;
9696
}
9797
}
9898
getCursor().putMessage(HAS_CONFLICT, hasConflict);
99-
if (!hasConflict) {
100-
maybeRemoveImport(FQN_WEB_SECURITY_CONFIGURER_ADAPTER);
101-
classDecl = classDecl.withExtends(null);
99+
maybeRemoveImport(FQN_WEB_SECURITY_CONFIGURER_ADAPTER);
100+
}
101+
classDecl = super.visitClassDeclaration(classDecl, context);
102+
if (!isWebSecurityConfigurerAdapterClass) {
103+
classDecl = processAnyClass(classDecl);
104+
} else if (!hasConflict) {
105+
classDecl = processSecurityAdapterClass(classDecl);
106+
}
107+
return classDecl;
108+
}
109+
110+
private J.ClassDeclaration processSecurityAdapterClass(J.ClassDeclaration classDecl) {
111+
classDecl = classDecl.withExtends(null);
112+
// Flatten configuration classes if applicable
113+
Cursor enclosingClassCursor = getCursor().getParent();
114+
while (enclosingClassCursor != null && !(enclosingClassCursor.getValue() instanceof J.ClassDeclaration)) {
115+
enclosingClassCursor = enclosingClassCursor.getParent();
116+
}
117+
if (enclosingClassCursor != null && enclosingClassCursor.getValue() instanceof J.ClassDeclaration) {
118+
J.ClassDeclaration enclosingClass = enclosingClassCursor.getValue();
119+
if (isMetaAnnotated(enclosingClass.getType(), FQN_CONFIGURATION, new HashSet<>()) && canMergeClassDeclarations(enclosingClass, classDecl)) {
120+
// can flatten. Outer class is annotated as configuration bean
121+
List<J.ClassDeclaration> classesToFlatten = enclosingClassCursor.getMessage(FLATTEN_CLASSES);
122+
if (classesToFlatten == null) {
123+
classesToFlatten = new ArrayList<>();
124+
enclosingClassCursor.putMessage(FLATTEN_CLASSES, classesToFlatten);
125+
}
126+
// only applicable to former subclasses of WebSecurityConfigurereAdapter - other classes won't be flattened
127+
classesToFlatten.add(classDecl);
128+
// Remove imports for annotations being removed together with class declaration
129+
// It is impossible in the general case to tell whether some of these annotations might apply to the bean methods
130+
// However, a set of hardcoded annotations can be moved in the future
131+
for (J.Annotation a : classDecl.getLeadingAnnotations()) {
132+
JavaType.FullyQualified type = TypeUtils.asFullyQualified(a.getType());
133+
if (type != null) {
134+
maybeRemoveImport(type);
135+
}
136+
}
137+
classDecl = null; // remove class
138+
}
139+
}
140+
return classDecl;
141+
}
142+
143+
private boolean canMergeClassDeclarations(J.ClassDeclaration a, J.ClassDeclaration b) {
144+
Set<String> aVars = getAllVarNames(a);
145+
Set<String> bVars = getAllVarNames(b);
146+
for (String av : aVars) {
147+
if (bVars.contains(av)) {
148+
return false;
102149
}
103150
}
104-
return super.visitClassDeclaration(classDecl, context);
151+
Set<String> aMethods = getAllMethodSignatures(a);
152+
Set<String> bMethods = getAllMethodSignatures(b);
153+
for (String am : aMethods) {
154+
if (bMethods.contains(am)) {
155+
return false;
156+
}
157+
}
158+
return true;
159+
}
160+
161+
private Set<String> getAllVarNames(J.ClassDeclaration c) {
162+
return c.getBody().getStatements().stream()
163+
.filter(J.VariableDeclarations.class::isInstance)
164+
.map(J.VariableDeclarations.class::cast)
165+
.flatMap(vd -> vd.getVariables().stream())
166+
.map(v -> v.getName().getSimpleName())
167+
.collect(Collectors.toSet());
168+
}
169+
170+
private Set<String> getAllMethodSignatures(J.ClassDeclaration c) {
171+
return c.getBody().getStatements().stream()
172+
.filter(J.MethodDeclaration.class::isInstance)
173+
.map(J.MethodDeclaration.class::cast)
174+
.map(this::simpleMethodSignature)
175+
.collect(Collectors.toSet());
176+
}
177+
178+
private String simpleMethodSignature(J.MethodDeclaration method) {
179+
String fullSignature = MethodMatcher.methodPattern(method);
180+
int firstSpaceIdx = fullSignature.indexOf(' ');
181+
return firstSpaceIdx < 0 ? fullSignature : fullSignature.substring(firstSpaceIdx + 1);
182+
}
183+
184+
private J.ClassDeclaration processAnyClass(J.ClassDeclaration classDecl) {
185+
// regular class case
186+
List<J.ClassDeclaration> toFlatten = getCursor().pollMessage(FLATTEN_CLASSES);
187+
if (toFlatten != null) {
188+
// The message won't be 'null' for a configuration class
189+
List<Statement> statements = new ArrayList<>(classDecl.getBody().getStatements().size() + toFlatten.size());
190+
statements.addAll(classDecl.getBody().getStatements());
191+
for (J.ClassDeclaration fc : toFlatten) {
192+
for (Statement s : fc.getBody().getStatements()) {
193+
if (s instanceof J.MethodDeclaration) {
194+
J.MethodDeclaration m = (J.MethodDeclaration) s;
195+
if (isAnnotatedWith(m.getLeadingAnnotations(), FQN_BEAN)) {
196+
JavaType.FullyQualified beanType = TypeUtils.asFullyQualified(m.getMethodType().getReturnType());
197+
String uniqueName = computeBeanNameFromClassName(fc.getSimpleName(), beanType.getClassName());
198+
s = m
199+
.withName(m.getName().withSimpleName(uniqueName))
200+
.withMethodType(m.getMethodType().withName(uniqueName));
201+
}
202+
}
203+
statements.add(s);
204+
}
205+
}
206+
classDecl = classDecl.withBody(classDecl.getBody().withStatements(statements));
207+
//TODO: not sure how to autoformat only the statements added to the class declaration
208+
doAfterVisit(new AutoFormatVisitor<>());
209+
}
210+
return classDecl;
105211
}
106212

107213
private boolean isConflictingMethod(@Nullable JavaType.Method methodType, String methodName) {
@@ -118,11 +224,13 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex
118224
if (isConflictingMethod(m.getMethodType(), method.getSimpleName())) {
119225
m = SearchResult.found(m, "Migrate manually based on https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter");
120226
} else if (!classCursor.getMessage(HAS_CONFLICT, true)) {
121-
if (CONFIGURE_HTTP_SECURITY_METHOD_MATCHER.matches(m, classCursor.getValue())) {
122-
JavaType securityChainType = JavaType.buildType(FQN_SECURITY_FILTER_CHAIN);
227+
J.ClassDeclaration c = classCursor.getValue();
228+
if (CONFIGURE_HTTP_SECURITY_METHOD_MATCHER.matches(m, c)) {
229+
JavaType.FullyQualified securityChainType = (JavaType.FullyQualified) JavaType.buildType(FQN_SECURITY_FILTER_CHAIN);
123230
JavaType.Method type = m.getMethodType();
231+
String newMethodName = "filterChain";
124232
if (type != null) {
125-
type = type.withName("filterChain").withReturnType(securityChainType);
233+
type = type.withName(newMethodName).withReturnType(securityChainType);
126234
}
127235

128236
Space returnPrefix = m.getReturnTypeExpression() == null ? Space.EMPTY : m.getReturnTypeExpression().getPrefix();
@@ -133,18 +241,19 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex
133241
}
134242
return anno;
135243
}))
136-
.withReturnTypeExpression(new J.Identifier(Tree.randomId(), returnPrefix, Markers.EMPTY,"SecurityFilterChain", securityChainType, null))
137-
.withName(m.getName().withSimpleName("filterChain"))
244+
.withReturnTypeExpression(new J.Identifier(Tree.randomId(), returnPrefix, Markers.EMPTY,securityChainType.getClassName(), securityChainType, null))
245+
.withName(m.getName().withSimpleName(newMethodName))
138246
.withMethodType(type)
139247
.withModifiers(ListUtils.map(m.getModifiers(), modifier -> EXPLICIT_ACCESS_LEVELS.contains(modifier.getType()) ? null : modifier));
140248

141249
m = addBeanAnnotation(m, getCursor());
142250
maybeAddImport(FQN_SECURITY_FILTER_CHAIN);
143251
} else if (CONFIGURE_WEB_SECURITY_METHOD_MATCHER.matches(m, classCursor.getValue())) {
144-
JavaType securityCustomizerType = JavaType.buildType(FQN_WEB_SECURITY_CUSTOMIZER);
252+
JavaType.FullyQualified securityCustomizerType = (JavaType.FullyQualified) JavaType.buildType(FQN_WEB_SECURITY_CUSTOMIZER);
145253
JavaType.Method type = m.getMethodType();
254+
String newMethodName = "webSecurityCustomizer";
146255
if (type != null) {
147-
type = type.withName("webSecurityCustomizer").withReturnType(securityCustomizerType);
256+
type = type.withName(newMethodName).withReturnType(securityCustomizerType);
148257
}
149258
Space returnPrefix = m.getReturnTypeExpression() == null ? Space.EMPTY : m.getReturnTypeExpression().getPrefix();
150259
m = m.withLeadingAnnotations(ListUtils.map(m.getLeadingAnnotations(), anno -> {
@@ -156,8 +265,8 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex
156265
}))
157266
.withMethodType(type)
158267
.withParameters(Collections.emptyList())
159-
.withReturnTypeExpression(new J.Identifier(Tree.randomId(), returnPrefix, Markers.EMPTY,"WebSecurityCustomizer", securityCustomizerType, null))
160-
.withName(m.getName().withSimpleName("webSecurityCustomizer"))
268+
.withReturnTypeExpression(new J.Identifier(Tree.randomId(), returnPrefix, Markers.EMPTY,securityCustomizerType.getClassName(), securityCustomizerType, null))
269+
.withName(m.getName().withSimpleName(newMethodName))
161270
.withModifiers(ListUtils.map(m.getModifiers(), modifier -> EXPLICIT_ACCESS_LEVELS.contains(modifier.getType()) ? null : modifier));
162271

163272
m = addBeanAnnotation(m, getCursor());
@@ -208,6 +317,44 @@ private J.MethodDeclaration addBeanAnnotation(J.MethodDeclaration m, Cursor c) {
208317
return m.withTemplate(template, m.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)));
209318
}
210319

320+
211321
};
212322
}
323+
324+
private static String computeBeanNameFromClassName(String className, String beanType) {
325+
String lowerCased = Character.toLowerCase(className.charAt(0)) + className.substring(1);
326+
String newName = lowerCased
327+
.replace("WebSecurityConfigurerAdapter", beanType)
328+
.replace("SecurityConfigurerAdapter", beanType)
329+
.replace("ConfigurerAdapter", beanType)
330+
.replace("Adapter", beanType);
331+
if (lowerCased.equals(newName)) {
332+
newName = newName + beanType;
333+
}
334+
return newName;
335+
}
336+
337+
private static boolean isMetaAnnotated(JavaType.FullyQualified t, String fqn, Set<JavaType.FullyQualified> visited) {
338+
for (JavaType.FullyQualified a : t.getAnnotations()) {
339+
if (!visited.contains(a)) {
340+
visited.add(a);
341+
if (fqn.equals(a.getFullyQualifiedName())) {
342+
return true;
343+
} else {
344+
boolean metaAnnotated = isMetaAnnotated(a, fqn, visited);
345+
if (metaAnnotated) {
346+
return true;
347+
}
348+
}
349+
}
350+
}
351+
return false;
352+
}
353+
354+
private static boolean isAnnotatedWith(Collection<J.Annotation> annotations, String annotationType) {
355+
return annotations.stream().anyMatch(a -> TypeUtils.isOfClassType(a.getType(), annotationType));
356+
}
357+
358+
359+
213360
}

0 commit comments

Comments
 (0)