2222import org .openrewrite .java .JavaParser ;
2323import org .openrewrite .java .JavaTemplate ;
2424import org .openrewrite .java .MethodMatcher ;
25+ import org .openrewrite .java .format .AutoFormatVisitor ;
2526import 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 .*;
3028import org .openrewrite .marker .Markers ;
3129import 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