diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/DefineMethod.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/DefineMethod.java index ba43f2699..601321449 100644 --- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/DefineMethod.java +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/DefineMethod.java @@ -18,12 +18,14 @@ import java.util.stream.Collectors; import org.openrewrite.ExecutionContext; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.JavaParser; import org.openrewrite.java.JavaTemplate; import org.openrewrite.java.MethodMatcher; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.JavaType.FullyQualified; public class DefineMethod extends Recipe { @@ -87,11 +89,22 @@ public class DefineMethod extends Recipe { .classpath(classpath.stream().map(s -> Paths.get(s)).collect(Collectors.toList()))) .imports(imports.toArray(new String[imports.size()])).build(); - J.Block body = t.apply(getCursor(), classDecl.getBody().getCoordinates().addMethodDeclaration((m, n) -> 1)); + // TODO: why did this return ClassDEclaration rather than Block??? Figure this out!!! + J.ClassDeclaration templateClass = t.apply(getCursor(), classDecl.getBody().getCoordinates().addMethodDeclaration((m, n) -> 1)); + FullyQualified classType = c.getType(); + if (classType != null) { + J.Block body = templateClass.getBody().withStatements(ListUtils.map(templateClass.getBody().getStatements(), s -> { + if (s instanceof J.MethodDeclaration) { + J.MethodDeclaration m = (J.MethodDeclaration) s; + return m.withMethodType(m.getMethodType().withDeclaringType(classType)); + } + return s; + })); + c = c.withBody(body); + } for (String fq : imports) { maybeAddImport(fq); } - c = c.withBody(body); } } return c; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index 0264ec611..bdd22520f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -14,14 +14,19 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.boot.java.reconcilers.AddConfigurationIfBeansPresentReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationNodeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.AuthorizeHttpRequestsReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.AutowiredFieldIntoConstructorParameterReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.BeanMethodNotPublicReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.BeanPostProcessingIgnoreInAotReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.HttpSecurityLambdaDslReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.ModulithTypeReferenceViolationReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.NoAutowiredOnConstructorReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.NoRepoAnnotationReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.NoRequestMappingAnnotationReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.NotRegisteredBeansReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.ServerHttpSecurityLambdaDslReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.WebSecurityConfigurerAdapterReconciler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; @@ -76,5 +81,25 @@ public class JdtConfig { @Bean UnnecessarySpringExtensionReconciler unnecessarySpringExtensionReconciler(SimpleLanguageServer server) { return new UnnecessarySpringExtensionReconciler(server.getQuickfixRegistry()); } + + @Bean HttpSecurityLambdaDslReconciler httpSecurityLamdaDslReconciler(SimpleLanguageServer server) { + return new HttpSecurityLambdaDslReconciler(server.getQuickfixRegistry()); + } + + @Bean ServerHttpSecurityLambdaDslReconciler serverHttpSecurityLambdaDslReconciler(SimpleLanguageServer server) { + return new ServerHttpSecurityLambdaDslReconciler(server.getQuickfixRegistry()); + } + + @Bean AuthorizeHttpRequestsReconciler authorizeHttpRequestsReconciler(SimpleLanguageServer server) { + return new AuthorizeHttpRequestsReconciler(server.getQuickfixRegistry()); + } + + @Bean BeanPostProcessingIgnoreInAotReconciler beanPostProcessingIgnoreInAotReconciler(SimpleLanguageServer server) { + return new BeanPostProcessingIgnoreInAotReconciler(server.getQuickfixRegistry()); + } + + @Bean NotRegisteredBeansReconciler notRegisteredBeansReconciler(SimpleLanguageServer server) { + return new NotRegisteredBeansReconciler(server.getQuickfixRegistry()); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AbstractSecurityLamdaDslReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AbstractSecurityLamdaDslReconciler.java new file mode 100644 index 000000000..c5f6963c4 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AbstractSecurityLamdaDslReconciler.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +import java.net.URI; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public abstract class AbstractSecurityLamdaDslReconciler implements JdtAstReconciler { + + private QuickfixRegistry registry; + + AbstractSecurityLamdaDslReconciler(QuickfixRegistry registry) { + this.registry = registry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + if (isCompleteAst) { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(MethodInvocation node) { + if (getApplicableMethodNames().contains(node.getName().getIdentifier()) && node.arguments().isEmpty()) { + ITypeBinding type = node.getExpression().resolveTypeBinding(); + if (type != null && getTargetTypeFqName().equals(type.getQualifiedName())) { + MethodInvocation topMethodInvocation = findTopLevelMethodInvocation(node); + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), getProblemLabel(), topMethodInvocation.getStartPosition(), topMethodInvocation.getLength()); + String uri = docUri.toASCIIString(); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of( + new FixDescriptor(getRecipeId(), List.of(uri), + RewriteQuickFixUtils.buildLabel(getFixLabel(), RecipeScope.NODE)) + .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, topMethodInvocation)) + .withRecipeScope(RecipeScope.NODE), + new FixDescriptor(getRecipeId(), List.of(uri), + RewriteQuickFixUtils.buildLabel(getFixLabel(), RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE), + new FixDescriptor(getRecipeId(), List.of(uri), + RewriteQuickFixUtils.buildLabel(getFixLabel(), RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT) + )); + problemCollector.accept(problem); + return false; + } + } + return true; + } + + }); + + } else { + if (RewriteQuickFixUtils.isAnyTypeUsed(cu, List.of(getTargetTypeFqName()))) { + throw new RequiredCompleteAstException(); + } + } + } + + private static MethodInvocation findTopLevelMethodInvocation(MethodInvocation m) { + for (; m.getParent() instanceof MethodInvocation; m = (MethodInvocation) m.getParent()) {} + return m; + } + + protected abstract String getFixLabel(); + + protected abstract String getRecipeId(); + + protected abstract String getProblemLabel(); + + abstract protected String getTargetTypeFqName(); + + abstract protected Collection getApplicableMethodNames(); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AuthorizeHttpRequestsReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AuthorizeHttpRequestsReconciler.java new file mode 100644 index 000000000..d36c2ede0 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AuthorizeHttpRequestsReconciler.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +import java.net.URI; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.SimpleType; +import org.openrewrite.java.spring.boot2.AuthorizeHttpRequests; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.java.Version; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class AuthorizeHttpRequestsReconciler implements JdtAstReconciler { + + private static final String ID = AuthorizeHttpRequests.class.getName(); + + private static final String FQN_HTTP_SECURITY = "org.springframework.security.config.annotation.web.builders.HttpSecurity"; + + private static final String AUTHORIZE_REQUESTS = "authorizeRequests"; + + private static final String AUTHORIZE_REQUESTS_PROBLEM_LABEL = "HttpSecurity API 'authorizeRequests(...)' is outdated"; + + private static final String AUTHORIZE_REQUESTS_FIX_LABEL = "Replace with 'authorizeHttpRequests(...)' and related types"; + + private static final String CLASS_FIX_LABEL_TEMPLATE = "Replace with %s and use 'HttpSecurity.authorizeHttpRequests(...) and related types"; + + private static final String FQN_AUTH_REQ_CONFIG = "org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer"; + + private static final String FQN_EXPR_AUTH_CONFIG = "org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer"; + + private static final String FQN_EXPR_INTERCEPT_REG = "org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry"; + + private QuickfixRegistry registry; + + public AuthorizeHttpRequestsReconciler(QuickfixRegistry registry) { + this.registry = registry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + if (isCompleteAst) { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(MethodInvocation node) { + if (AUTHORIZE_REQUESTS.equals(node.getName().getIdentifier()) && node.arguments().isEmpty()) { + ITypeBinding type = node.getExpression().resolveTypeBinding(); + if (type != null && FQN_HTTP_SECURITY.equals(type.getQualifiedName())) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), + AUTHORIZE_REQUESTS_PROBLEM_LABEL, node.getName().getStartPosition(), + node.getName().getLength()); + String uri = docUri.toASCIIString(); + RewriteQuickFixUtils + .setRewriteFixes(registry, problem, List.of( + new FixDescriptor(ID, List.of(uri), + RewriteQuickFixUtils.buildLabel(AUTHORIZE_REQUESTS_FIX_LABEL, + RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE), + new FixDescriptor(ID, List.of(uri), + RewriteQuickFixUtils.buildLabel(AUTHORIZE_REQUESTS_FIX_LABEL, + RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT))); + problemCollector.accept(problem); + return false; + } + } + return true; + } + + @Override + public boolean visit(SimpleType node) { + String replacementClass = null; + if (RewriteQuickFixUtils.isApplicableTypeWithoutResolving(cu, + List.of(FQN_AUTH_REQ_CONFIG, FQN_EXPR_AUTH_CONFIG), node.getName())) { + replacementClass = "AuthorizeHttpRequestsConfigurer"; + } else if (RewriteQuickFixUtils.isApplicableTypeWithoutResolving(cu, List.of(FQN_EXPR_INTERCEPT_REG), + node.getName())) { + replacementClass = "AuthorizationManagerRequestMatcherRegistry"; + } + if (replacementClass != null) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), + "Use of type '" + node.getName().getFullyQualifiedName() + "' is outdated", + node.getName().getStartPosition(), node.getName().getLength()); + String uri = docUri.toASCIIString(); + RewriteQuickFixUtils + .setRewriteFixes(registry, problem, List.of( + new FixDescriptor(ID, List.of(uri), + RewriteQuickFixUtils.buildLabel(String.format(CLASS_FIX_LABEL_TEMPLATE, + replacementClass), RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE), + new FixDescriptor(ID, List.of(uri), + RewriteQuickFixUtils.buildLabel( + String.format(CLASS_FIX_LABEL_TEMPLATE, replacementClass), + RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT))); + problemCollector.accept(problem); + return false; + } + return super.visit(node); + } + + }); + } else { + boolean needsFullAst = RewriteQuickFixUtils.isAnyTypeUsed(cu, List.of( + FQN_HTTP_SECURITY, + FQN_AUTH_REQ_CONFIG, + FQN_EXPR_AUTH_CONFIG, + FQN_EXPR_INTERCEPT_REG + )); + if (needsFullAst) { + throw new RequiredCompleteAstException(); + } + } + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, "spring-security-config"); + return version != null && version.compareTo(new Version(5, 6, 0, null)) >= 0; + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.HTTP_SECURITY_AUTHORIZE_HTTP_REQUESTS; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java index 946db123f..48c231156 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java @@ -85,10 +85,12 @@ public class BeanMethodNotPublicReconciler implements JdtAstReconciler { private static final boolean isOverriding(IMethodBinding binding) { try { - Field f = binding.getClass().getDeclaredField("binding"); - f.setAccessible(true); - org.eclipse.jdt.internal.compiler.lookup.MethodBinding value = (org.eclipse.jdt.internal.compiler.lookup.MethodBinding) f.get(binding); - return value.isOverriding(); + if (binding != null) { + Field f = binding.getClass().getDeclaredField("binding"); + f.setAccessible(true); + org.eclipse.jdt.internal.compiler.lookup.MethodBinding value = (org.eclipse.jdt.internal.compiler.lookup.MethodBinding) f.get(binding); + return value.isOverriding(); + } } catch (Exception e) { log.error("", e); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanPostProcessingIgnoreInAotReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanPostProcessingIgnoreInAotReconciler.java new file mode 100644 index 000000000..fcd3496e6 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanPostProcessingIgnoreInAotReconciler.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.ReturnStatement; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.BeanPostProcessingIgnoreInAot; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class BeanPostProcessingIgnoreInAotReconciler implements JdtAstReconciler { + + private static final String RECIPE_ID = BeanPostProcessingIgnoreInAot.class.getName(); + private static final String LABEL = "Add method 'isBeanExcludedFromAotProcessing' that returns 'false'"; + private static final String RUNTIME_BEAN_POST_PROCESSOR = "org.springframework.beans.factory.config.BeanPostProcessor"; + private static final String COMPILE_BEAN_POST_PROCESSOR = "org.springframework.beans.factory.aot.BeanRegistrationAotProcessor"; + public static final String METHOD_NAME = "isBeanExcludedFromAotProcessing"; + + private QuickfixRegistry registry; + + public BeanPostProcessingIgnoreInAotReconciler(QuickfixRegistry registry) { + this.registry = registry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + AtomicBoolean requiresFullAst = new AtomicBoolean(false); + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(TypeDeclaration typeDecl) { + ITypeBinding type = typeDecl.resolveBinding(); + if (type != null && isApplicable(type)) { + MethodDeclaration foundMethod = null; + boolean markProblem = false; + for (MethodDeclaration m : typeDecl.getMethods()) { + if (METHOD_NAME.equals(m.getName().getIdentifier()) && m.parameters().isEmpty()) { + foundMethod = m; + break; + } + } + if (foundMethod != null) { + if (isCompleteAst) { + AtomicBoolean returnsTrue = new AtomicBoolean(false); + foundMethod.accept(new ASTVisitor() { + @Override + public boolean visit(ReturnStatement node) { + if (Boolean.TRUE.equals(node.getExpression().resolveConstantExpressionValue())) { + returnsTrue.set(true); + } + return !returnsTrue.get(); + } + }); + markProblem = returnsTrue.get(); + } else { + requiresFullAst.set(true); + } + } else { + markProblem = true; + } + + if (markProblem) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, typeDecl.getName().getStartPosition(), typeDecl.getName().getLength()); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of( + new FixDescriptor(RECIPE_ID, List.of(docUri.toASCIIString()), RewriteQuickFixUtils.buildLabel(LABEL, RecipeScope.NODE)) + .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, typeDecl)) + .withRecipeScope(RecipeScope.NODE) + )); + problemCollector.accept(problem); + } + } + return !requiresFullAst.get(); + } + + private boolean isApplicable(ITypeBinding type) { + return RewriteQuickFixUtils.implementsType(RUNTIME_BEAN_POST_PROCESSOR, type) && RewriteQuickFixUtils.implementsType(COMPILE_BEAN_POST_PROCESSOR, type); + } + + }); + if (requiresFullAst.get()) { + throw new RequiredCompleteAstException(); + } + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(3, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return SpringAotJavaProblemType.JAVA_BEAN_POST_PROCESSOR_IGNORED_IN_AOT; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/EntityIdForRepoReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/EntityIdForRepoReconciler.java new file mode 100644 index 000000000..be825744e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/EntityIdForRepoReconciler.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.net.URI; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.openrewrite.java.spring.boot2.search.EntityIdForRepositoryVisitor; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; + +public class EntityIdForRepoReconciler implements JdtAstReconciler { + + private static final String ID = EntityIdForRepositoryVisitor.class.getName(); + + private QuickfixRegistry registry; + + public EntityIdForRepoReconciler(QuickfixRegistry registry) { + this.registry = registry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + final boolean considerIdField = SpringProjectUtil.hasSpecificLibraryOnClasspath(project, "spring-data-mongodb-", true); + cu.accept(new ASTVisitor() { + }); + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(2, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.DOMAIN_ID_FOR_REPOSITORY; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/HttpSecurityLambdaDslReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/HttpSecurityLambdaDslReconciler.java new file mode 100644 index 000000000..238463896 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/HttpSecurityLambdaDslReconciler.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +import java.util.Arrays; +import java.util.Collection; + +import org.openrewrite.java.spring.boot2.HttpSecurityLambdaDsl; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.java.Version; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; + +public class HttpSecurityLambdaDslReconciler extends AbstractSecurityLamdaDslReconciler { + + private static final Collection APPLICABLE_METHOD_NAMES = Arrays.asList( + "anonymous", "authorizeRequests", "cors", "csrf", "exceptionHandling", "formLogin", + "headers", "httpBasic", "jee", "logout", "oauth2Client", "oauth2Login", "oauth2ResourceServer", + "openidLogin", "portMapper", "rememberMe", "requestCache", "requestMatchers", "requiresChannel", + "saml2Login", "securityContext", "servletApi", "sessionManagement", "x509"); + + + public HttpSecurityLambdaDslReconciler(QuickfixRegistry registry) { + super(registry); + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, "spring-security-config"); + return version != null && version.compareTo(new Version(5, 2, 0, null)) >= 0; + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_LAMBDA_DSL; + } + + @Override + protected String getFixLabel() { + return "Switch to 'HttpSecurity` Lambda DSL syntax"; + } + + @Override + protected String getRecipeId() { + return HttpSecurityLambdaDsl.class.getName(); + } + + @Override + protected String getProblemLabel() { + return "Consider switching to 'HttpSecurity' Lambda DSL syntax"; + } + + @Override + protected String getTargetTypeFqName() { + return "org.springframework.security.config.annotation.web.builders.HttpSecurity"; + } + + @Override + protected Collection getApplicableMethodNames() { + return APPLICABLE_METHOD_NAMES; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java new file mode 100644 index 000000000..82810bbcd --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.beans.BeansSymbolAddOnInformation; +import org.springframework.ide.vscode.boot.java.beans.ConfigBeanSymbolAddOnInformation; +import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; +import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.DefineMethod; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.google.common.collect.ImmutableSet; + +public class NotRegisteredBeansReconciler implements JdtAstReconciler, ApplicationContextAware { + + private static final String DEFINE_METHOD_RECIPE = DefineMethod.class.getName(); + + private static final List AOT_BEANS = List.of( + "org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor", + "org.springframework.beans.factory.aot.BeanRegistrationAotProcessor" + ); + + private ApplicationContext applicationContext; + + private QuickfixRegistry registry; + + public NotRegisteredBeansReconciler(QuickfixRegistry registry) { + this.registry = registry; + + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(TypeDeclaration node) { + if (!node.isInterface() && !Modifier.isAbstract(node.getModifiers())) { + ITypeBinding type = node.resolveBinding(); + if (type != null && RewriteQuickFixUtils.implementsAnyType(AOT_BEANS, type)) { + String beanClassName =type.getQualifiedName(); + SpringSymbolIndex index = applicationContext.getBean(SpringSymbolIndex.class); + List beanSymbols = index.getSymbols(data -> { + SymbolAddOnInformation[] additionalInformation = data.getAdditionalInformation(); + if (additionalInformation != null) { + for (SymbolAddOnInformation info : additionalInformation) { + if (info instanceof BeansSymbolAddOnInformation) { + BeansSymbolAddOnInformation info2 = (BeansSymbolAddOnInformation) info; + return beanClassName.equals(info2.getBeanType()); + } + } + } + return false; + }).limit(1).collect(Collectors.toList()); + + if (beanSymbols.isEmpty()) { + Builder fixListBuilder = ImmutableList.builder(); + for (EnhancedSymbolInformation s : index.getEnhancedSymbols(project)) { + if (s.getAdditionalInformation() != null) { + ConfigBeanSymbolAddOnInformation configInfo = Arrays.stream(s.getAdditionalInformation()).filter(ConfigBeanSymbolAddOnInformation.class::isInstance).map(ConfigBeanSymbolAddOnInformation.class::cast).findFirst().orElse(null); + if (configInfo != null) { + for (IMethodBinding constructor : type.getDeclaredMethods()) { + if (constructor.isConstructor()) { + String constructorParamsSignature = "(" + Arrays.stream(constructor.getParameterTypes()).map(pt -> typePattern(pt)).collect(Collectors.joining(",")) + ")"; + String beanMethodName = "get" + type.getName(); + String pattern = beanMethodName + constructorParamsSignature; + String contructorParamsLabel = "(" + Arrays.stream(constructor.getParameterTypes()).map(NotRegisteredBeansReconciler::typeStr).collect(Collectors.joining(", ")) + ")"; + + Builder paramBuilder = ImmutableList.builder(); + for (int i = 0; i < constructor.getParameterNames().length && i < constructor.getParameterTypes().length; i++) { + ITypeBinding paramType = constructor.getParameterTypes()[i]; + String paramName = constructor.getParameterNames()[i]; + paramBuilder.add(typeStr(paramType) + ' ' + paramName); + } + String paramsStr = String.join(", ", paramBuilder.build().toArray(String[]::new)); + + fixListBuilder.add(new FixDescriptor(DEFINE_METHOD_RECIPE, List.of(s.getSymbol().getLocation().getLeft().getUri()), "Define bean in config '" + configInfo.getBeanID() + "' with constructor " + contructorParamsLabel) + .withRecipeScope(RecipeScope.FILE) + .withParameters(Map.of( + "targetFqName", configInfo.getBeanType(), + "signature", pattern, + "template", "@Bean\n" + + type.getName() + " " + beanMethodName + "(" + paramsStr + ") {\n" + + "return new " + type.getName() + "(" + Arrays.stream(constructor.getParameterNames()).collect(Collectors.joining(", ")) + ");\n" + + "}\n", + "imports", allFQTypes(constructor).toArray(String[]::new), + "typeStubs", new String[0]/*new String[] { source.printAll() }*/, + "classpath", IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath().toString()).toArray(String[]::new) + + )) + ); + } + } + } + } + } + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), getProblemType().getLabel(), node.getName().getStartPosition(), node.getName().getLength()); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, fixListBuilder.build()); + problemCollector.accept(problem); + } + } + } + return super.visit(node); + } + + }); + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(3, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + private static Set allFQTypes(IBinding binding) { + ImmutableSet.Builder b = ImmutableSet.builder(); + if (binding instanceof IMethodBinding) { + IMethodBinding methodBinding = (IMethodBinding) binding; + b.addAll(allFQTypes(methodBinding.getDeclaringClass())); + for (ITypeBinding paramType : methodBinding.getParameterTypes()) { + b.addAll(allFQTypes(paramType)); + } + } else if (binding instanceof ITypeBinding) { + ITypeBinding typeBinding = (ITypeBinding) binding; + if (!typeBinding.isPrimitive()) { + for (ITypeBinding paramType : typeBinding.getTypeParameters()) { + b.addAll(allFQTypes(paramType)); + } + for (ITypeBinding argType : typeBinding.getTypeArguments()) { + b.addAll(allFQTypes(argType)); + + } + b.addAll(allFQTypes(typeBinding.getBound())); + ITypeBinding erasure = typeBinding.getErasure(); + if (erasure == typeBinding) { + b.add(typeBinding.getQualifiedName()); + } else { + b.addAll(allFQTypes(erasure)); + } + } + } + return b.build(); + } + + + private static String typePattern(ITypeBinding type) { + if (type.isArray()) { + return typePattern(type.getErasure()) + "[]"; + } else { + return RewriteQuickFixUtils.getDeepErasureType(type).getQualifiedName(); + } + } + + private static String typeStr(ITypeBinding type) { + return type.getName(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java index e88d17de1..2bd166e3e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java @@ -12,12 +12,17 @@ package org.springframework.ide.vscode.boot.java.reconcilers; import java.util.Collection; import java.util.Iterator; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.BodyDeclaration; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.SimpleType; import org.openrewrite.Tree; import org.openrewrite.marker.Range; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; @@ -90,6 +95,87 @@ public class RewriteQuickFixUtils { return type; } + public static boolean isApplicableTypeWithoutResolving(CompilationUnit cu, Collection types, Name typeNameNode) { + String typeName = typeNameNode.getFullyQualifiedName(); + if (cu.getPackage().getName() != null && types.contains(cu.getPackage().getName().getFullyQualifiedName() + "." + typeName)) { + return true; + } + if (types.contains(typeName)) { + return true; + } else if (types.stream().anyMatch(t -> t.endsWith(typeName))) { + for (Object im : cu.imports()) { + ImportDeclaration importDecl = (ImportDeclaration) im; + String importFqName = importDecl.getName().getFullyQualifiedName(); + if (importDecl.isOnDemand()) { + if (types.contains(importFqName + "." + typeName)) { + return true; + } + } else { + String importSimpleName = getSimpleName(importFqName); + String firstTokenOfTypeName = getFirstTokenBeforeDot(typeName); + if (importSimpleName.equals(firstTokenOfTypeName)) { + if (types.contains(importFqName + typeName.substring(firstTokenOfTypeName.length()))) { + return true; + } + } + } + } + } + return false; + } + + public static boolean isAnyTypeUsed(CompilationUnit cu, Collection types) { + AtomicBoolean typeUsed = new AtomicBoolean(false); + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(ImportDeclaration node) { + String fqName = node.getName().getFullyQualifiedName(); + if (types.contains(fqName)) { + typeUsed.set(true); + } + return !typeUsed.get(); + } + + @Override + public boolean visit(SimpleType node) { + if (RewriteQuickFixUtils.isApplicableTypeWithoutResolving(cu, types, node.getName())) { + typeUsed.set(true); + } + return !typeUsed.get(); + } + }); + return typeUsed.get(); + } + + public static boolean implementsType(String fqName, ITypeBinding type) { + if (fqName.equals(type.getQualifiedName())) { + return true; + } else { + for (ITypeBinding t : type.getInterfaces()) { + if (implementsType(fqName, t)) { + return true; + } + } + } + return false; + } + + public static boolean implementsAnyType(Collection fqNames, ITypeBinding type) { + if (fqNames.contains(type.getQualifiedName())) { + return true; + } else { + for (ITypeBinding t : type.getInterfaces()) { + if (implementsAnyType(fqNames, t)) { + return true; + } + } + } + return false; + } + + + public static String getSimpleName(String fqName) { int idx = fqName.lastIndexOf('.'); if (idx >= 0 && idx < fqName.length() - 1) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ServerHttpSecurityLambdaDslReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ServerHttpSecurityLambdaDslReconciler.java new file mode 100644 index 000000000..004172991 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ServerHttpSecurityLambdaDslReconciler.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers; + +import java.util.Arrays; +import java.util.Collection; + +import org.openrewrite.java.spring.boot2.ServerHttpSecurityLambdaDsl; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.java.Version; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; + +public class ServerHttpSecurityLambdaDslReconciler extends AbstractSecurityLamdaDslReconciler { + + private static final Collection APPLICABLE_METHOD_NAMES = Arrays.asList("anonymous", "authorizeExchange", + "cors", "csrf", "exceptionHandling", "formLogin", "headers", "httpBasic", "logout", "oauth2Client", + "oauth2Login", "oauth2ResourceServer", "redirectToHttps", "requestCache", "x509"); + + public ServerHttpSecurityLambdaDslReconciler(QuickfixRegistry registry) { + super(registry); + // TODO Auto-generated constructor stub + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, "spring-security-config"); + return version != null && version.compareTo(new Version(5, 2, 0, null)) >= 0; + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_LAMBDA_DSL; + } + + @Override + protected String getFixLabel() { + return "Switch to 'ServerHttpSecurity` Lambda DSL syntax"; + } + + @Override + protected String getRecipeId() { + return ServerHttpSecurityLambdaDsl.class.getName(); + } + + @Override + protected String getProblemLabel() { + return "Consider switching to 'ServerHttpSecurity' Lambda DSL syntax"; + } + + @Override + protected String getTargetTypeFqName() { + return "org.springframework.security.config.web.server.ServerHttpSecurity"; + } + + @Override + protected Collection getApplicableMethodNames() { + return APPLICABLE_METHOD_NAMES; + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/AuthorizeHttpRequestsReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/AuthorizeHttpRequestsReconcilerTest.java new file mode 100644 index 000000000..78319b206 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/AuthorizeHttpRequestsReconcilerTest.java @@ -0,0 +1,198 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.AuthorizeHttpRequestsReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class AuthorizeHttpRequestsReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "authorizerequests"; + } + + @Override + protected String getProjectName() { + return "test-spring-indexing"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new AuthorizeHttpRequestsReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void requireFullAst_1() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + + class A { + + void something(HttpSecurity security) { + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void requireFullAst_2() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + + class A { + + void something() { + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void requireFullAst_3() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + + class A { + + void something(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry reg) { + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void sanityTest() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + + class A { + + void something(HttpSecurity security) { + security.authorizeRequests().mvcMatchers(); + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.HTTP_SECURITY_AUTHORIZE_HTTP_REQUESTS, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("authorizeRequests", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + } + + @Test + void multipleProblems() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + import org.springframework.security.web.SecurityFilterChain; + + @Configuration + public class JdbcSecurityConfiguration { + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry reqs = http.authorizeRequests(); + reqs.antMatchers("/ll").authenticated(); + return http.build(); + } + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(2, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.HTTP_SECURITY_AUTHORIZE_HTTP_REQUESTS, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("ExpressionUrlAuthorizationConfigurer", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + problem = problems.get(1); + + assertEquals(Boot2JavaProblemType.HTTP_SECURITY_AUTHORIZE_HTTP_REQUESTS, problem.getType()); + + markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("authorizeRequests", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java index b7d7222e7..9942f3a5c 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java @@ -21,6 +21,7 @@ import java.nio.file.Paths; import java.util.Comparator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.FileASTRequestor; @@ -42,7 +43,7 @@ public abstract class BaseReconcilerTest { abstract protected JdtAstReconciler getReconciler(); - private Path createFile(String name, String content) throws IOException { + protected Path createFile(String name, String content) throws IOException { Path filePath = Paths.get(project.getLocationUri()).resolve("src/main/java").resolve(getFolder()).resolve(name); Files.createDirectories(filePath.getParent()); Files.createFile(filePath); @@ -75,6 +76,10 @@ public abstract class BaseReconcilerTest { } List reconcile(String fileName, String source, boolean isCompleteAst) throws Exception { + return reconcile(this::getReconciler, fileName, source, isCompleteAst); + } + + List reconcile(Supplier reconcilerFactory, String fileName, String source, boolean isCompleteAst) throws Exception { Path path = createFile(fileName, source); TestProblemCollector problemCollector = new TestProblemCollector(); AtomicBoolean requiredCompleteAst = new AtomicBoolean(false); @@ -83,7 +88,7 @@ public abstract class BaseReconcilerTest { @Override public void acceptAST(String sourceFilePath, CompilationUnit cu) { try { - getReconciler().reconcile(project, path.toUri(), cu, problemCollector, isCompleteAst); + reconcilerFactory.get().reconcile(project, path.toUri(), cu, problemCollector, isCompleteAst); } catch (RequiredCompleteAstException e) { requiredCompleteAst.set(true); } @@ -97,5 +102,4 @@ public abstract class BaseReconcilerTest { return problemCollector.getCollectedProblems(); } - } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanPostProcessingIgnoreInAotReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanPostProcessingIgnoreInAotReconcilerTest.java new file mode 100644 index 000000000..b941c6d60 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanPostProcessingIgnoreInAotReconcilerTest.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.BeanPostProcessingIgnoreInAotReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class BeanPostProcessingIgnoreInAotReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "beanpostprocessingaot"; + } + + @Override + protected String getProjectName() { + return "test-spring-validations"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new BeanPostProcessingIgnoreInAotReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void noMethod() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.config.BeanPostProcessor; + import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; + + class A implements BeanPostProcessor, BeanRegistrationAotProcessor{ + + A() {}; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_BEAN_POST_PROCESSOR_IGNORED_IN_AOT, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("A", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + + } + + @Test + void withMethodReturningTrue_IncompleteAst() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.config.BeanPostProcessor; + import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; + + class A implements BeanPostProcessor, BeanRegistrationAotProcessor{ + + A() {}; + + public boolean isBeanExcludedFromAotProcessing() { return true; } + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require complete AST"); + } catch (RequiredCompleteAstException e) { + // good + } + } + + + @Test + void withMethodReturningTrue_CompleteAst() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.config.BeanPostProcessor; + import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; + + class A implements BeanPostProcessor, BeanRegistrationAotProcessor{ + + A() {}; + + public boolean isBeanExcludedFromAotProcessing() { return true; } + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_BEAN_POST_PROCESSOR_IGNORED_IN_AOT, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("A", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + + } + + @Test + void withMethodReturningFalse() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.config.BeanPostProcessor; + import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; + + class A implements BeanPostProcessor, BeanRegistrationAotProcessor{ + + A() {}; + + public boolean isBeanExcludedFromAotProcessing() { return false; } + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } + + @Test + void noBeanPostProcessor() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; + + class A implements BeanRegistrationAotProcessor{ + + A() {}; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/HttpSecurityLambdaDslReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/HttpSecurityLambdaDslReconcilerTest.java new file mode 100644 index 000000000..84127c294 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/HttpSecurityLambdaDslReconcilerTest.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.HttpSecurityLambdaDslReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class HttpSecurityLambdaDslReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "httpsecuritydsl"; + } + + @Override + protected String getProjectName() { + return "test-spring-indexing"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new HttpSecurityLambdaDslReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void requireFullAst_1() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + + class A { + + void something(HttpSecurity security) { + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void requireFullAst_2() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.builders.*; + + class A { + + void something(HttpSecurity security) { + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void requireFullAst_3() throws Exception { + String source = """ + package example.demo; + + class A { + + void something(org.springframework.security.config.annotation.web.builders.HttpSecurity security) { + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void sanityTest() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + + class A { + + void something(HttpSecurity security) { + security.authorizeRequests().mvcMatchers(); + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_LAMBDA_DSL, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("security.authorizeRequests().mvcMatchers()", markedStr); + + assertEquals(3, problem.getQuickfixes().size()); + + } + + @Test + void noProblem() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + + class A { + + void something(HttpSecurity http) { + http + .authorizeRequests(requests -> requests + .antMatchers("/blog/**").permitAll() + .anyRequest().authenticated()) + .formLogin(login -> login + .loginPage("/login") + .permitAll()) + .rememberMe(withDefaults()); + + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansReconcilerTest.java new file mode 100644 index 000000000..01d80b8ed --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NotRegisteredBeansReconcilerTest.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.beans.ConfigBeanSymbolAddOnInformation; +import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; +import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.NotRegisteredBeansReconciler; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + + +public class NotRegisteredBeansReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "notregisteredbeanaot"; + } + + @Override + protected String getProjectName() { + return "test-spring-validations"; + } + + @SuppressWarnings("unchecked") + @Override + protected JdtAstReconciler getReconciler() { + NotRegisteredBeansReconciler reconciler = new NotRegisteredBeansReconciler(new QuickfixRegistry()); + SpringSymbolIndex mockSymbolIndex = mock(SpringSymbolIndex.class); + when(mockSymbolIndex.getSymbols(any(Predicate.class))).thenReturn(Stream.empty()); + + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(SpringSymbolIndex.class)).thenReturn(mockSymbolIndex); + + reconciler.setApplicationContext(context); + return reconciler; + } + + @SuppressWarnings("unchecked") + private NotRegisteredBeansReconciler createReconciler(EnhancedSymbolInformation... beanSymbols) { + NotRegisteredBeansReconciler reconciler = new NotRegisteredBeansReconciler(new QuickfixRegistry()); + SpringSymbolIndex mockSymbolIndex = mock(SpringSymbolIndex.class); + when(mockSymbolIndex.getSymbols(any(Predicate.class))).thenReturn(Stream.empty()); + when(mockSymbolIndex.getEnhancedSymbols(any(IJavaProject.class))).thenReturn(Arrays.asList(beanSymbols)); + + ApplicationContext context = mock(ApplicationContext.class); + when(context.getBean(SpringSymbolIndex.class)).thenReturn(mockSymbolIndex); + + reconciler.setApplicationContext(context); + return reconciler; + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void sanityTest() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; + + class A implements BeanRegistrationAotProcessor { + + public A(String k) {} + } + """; + List problems = reconcile(() -> createReconciler(), "A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("A", markedStr); + + assertEquals(0, problem.getQuickfixes().size()); + + } + + @Test + void sanityTestWithQuickFixes() throws Exception { + Path configClassPath = createFile("TestConfig.java", """ + package example.demo; + + import org.springframework.context.annotation.Configuration; + + @Configuration + class TestConfig { + } + """); + + String source = """ + package example.demo; + + import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; + + class A implements BeanRegistrationAotProcessor { + + public A(String k) {} + } + """; + + WorkspaceSymbol workspaceSymbol = new WorkspaceSymbol("testConfig", SymbolKind.Class, Either.forLeft(new Location(configClassPath.toUri().toASCIIString(), new Range()))); + ConfigBeanSymbolAddOnInformation configBeanAddOn = new ConfigBeanSymbolAddOnInformation("testConfig", "example.demo.TestConfig"); + + List problems = reconcile(() -> createReconciler(new EnhancedSymbolInformation(workspaceSymbol, new SymbolAddOnInformation[] { configBeanAddOn })), "A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("A", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/ServerHttpSecurityLambdaDslReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/ServerHttpSecurityLambdaDslReconcilerTest.java new file mode 100644 index 000000000..9a1e6e515 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/ServerHttpSecurityLambdaDslReconcilerTest.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.boot.java.reconcilers.ServerHttpSecurityLambdaDslReconciler; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class ServerHttpSecurityLambdaDslReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "serverhttpsecuritydsl"; + } + + @Override + protected String getProjectName() { + return "test-spring-indexing"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new ServerHttpSecurityLambdaDslReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void requireFullAst() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.web.server.ServerHttpSecurity; + + class A { + + void something(ServerHttpSecurity security) { + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void sanityTest() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.web.server.ServerHttpSecurity; + + class A { + + void something(ServerHttpSecurity http) { + http.authorizeExchange().pathMatchers("/blog/**").permitAll().anyExchange().authenticated(); + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_LAMBDA_DSL, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("http.authorizeExchange().pathMatchers(\"/blog/**\").permitAll().anyExchange().authenticated()", markedStr); + + assertEquals(3, problem.getQuickfixes().size()); + + } + + @Test + void noProblem() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.web.server.ServerHttpSecurity; + + class A { + + void something(ServerHttpSecurity http) { + http.authorizeExchange(exchange -> exchange.pathMatchers("/blog/**").permitAll().anyExchange().authenticated()); + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } + +}