From 7df37a350981eefc3135562d582085972d2b0cb1 Mon Sep 17 00:00:00 2001 From: aboyko Date: Wed, 12 Mar 2025 17:53:46 -0400 Subject: [PATCH] Validation and Quickfix for RegistrarBean --- .../ImportBeanRegistrarInConfigRecipe.java | 199 ++++++++++++++ ...ImportBeanRegistrarInConfigRecipeTest.java | 192 +++++++++++++ .../ide/vscode/boot/app/JdtConfig.java | 8 +- .../boot/common/SpringProblemCategories.java | 5 +- .../boot/java/Boot4JavaProblemType.java | 84 ++++++ .../java/beans/ComponentSymbolProvider.java | 4 +- .../BeanRegistrarDeclarationReconciler.java | 117 ++++++++ .../ide/vscode/boot/java/utils/ASTUtils.java | 6 +- .../vscode/boot/java/utils/FunctionUtils.java | 2 +- ...eanRegistrarDeclarationReconcilerTest.java | 244 +++++++++++++++++ .../.mvn/wrapper/maven-wrapper.properties | 19 ++ .../test-projects/sf7-validation/mvnw | 259 ++++++++++++++++++ .../test-projects/sf7-validation/mvnw.cmd | 149 ++++++++++ .../test-projects/sf7-validation/pom.xml | 120 ++++++++ .../main/java/com/example/demo/Config.java | 8 + .../demo/Sf7ValidationApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../demo/Sf7ValidationApplicationTests.java | 13 + 18 files changed, 1435 insertions(+), 8 deletions(-) create mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipe.java create mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipeTest.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot4JavaProblemType.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanRegistrarDeclarationReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/.mvn/wrapper/maven-wrapper.properties create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw.cmd create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Config.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Sf7ValidationApplication.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/resources/application.properties create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/test/java/com/example/demo/Sf7ValidationApplicationTests.java diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipe.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipe.java new file mode 100644 index 000000000..0bc4466fc --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipe.java @@ -0,0 +1,199 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.rewrite.java; + +import java.util.ArrayList; +import java.util.List; + +import org.openrewrite.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.NlsRewrite.Description; +import org.openrewrite.NlsRewrite.DisplayName; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.AnnotationMatcher; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.search.DeclaresType; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.J.Annotation; +import org.openrewrite.java.tree.J.ClassDeclaration; +import org.openrewrite.java.tree.J.NewArray; +import org.openrewrite.java.tree.JLeftPadded; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.JavaType.FullyQualified; +import org.openrewrite.java.tree.JavaType.ShallowClass; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.TypeTree; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.marker.Markers; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ImportBeanRegistrarInConfigRecipe extends Recipe { + + private static final String IMPORT_FQN = "org.springframework.context.annotation.Import"; + + private String configBeanFqn; + + private String beanRegFqn; + + @JsonCreator + public ImportBeanRegistrarInConfigRecipe( + @JsonProperty("configBeanFqn") String configBeanFqn, + @JsonProperty("beanRegFqn") String beanRegFqn) { + this.configBeanFqn = configBeanFqn; + this.beanRegFqn = beanRegFqn; + } + + @Override + public @DisplayName String getDisplayName() { + return "Add `BeanRegistrar` with `@Import` in Configuration bean"; + } + + @Override + public @Description String getDescription() { + return "Add `BeanRegistrar` with `@Import` in Configuration bean."; + } + + @Override + public TreeVisitor getVisitor() { + final AnnotationMatcher importAnnotationMatcher = new AnnotationMatcher("@" + IMPORT_FQN); + final ShallowClass beanRegistrarType = JavaType.ShallowClass.build(beanRegFqn); + return Preconditions.check(new DeclaresType<>(configBeanFqn), new JavaIsoVisitor<>() { + + @Override + public ClassDeclaration visitClassDeclaration(ClassDeclaration classDecl, ExecutionContext p) { + ClassDeclaration cd = classDecl; + FullyQualified type = TypeUtils.asFullyQualified(cd.getType()); + + if (type != null && configBeanFqn.equals(type.getFullyQualifiedName())) { + J.Annotation importAnnotation = cd.getLeadingAnnotations().stream().filter(importAnnotationMatcher::matches).findFirst().orElse(null); + if (importAnnotation == null) { + ArrayList annotations = new ArrayList<>(cd.getLeadingAnnotations()); + JavaType.ShallowClass annotationType = JavaType.ShallowClass.build(IMPORT_FQN); + J.Identifier typeName = new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, List.of(), annotationType.getClassName(), annotationType, null); + Space indent = Space.build("\n" + cd.getPrefix().getIndent(), List.of()); + boolean noAnnotations = annotations.isEmpty(); + importAnnotation = new J.Annotation( + Tree.randomId(), + noAnnotations ? Space.EMPTY : indent, + Markers.EMPTY, + typeName, + null); + annotations.add(importAnnotation); + cd = cd.withLeadingAnnotations(annotations); + if (noAnnotations) { + cd = cd.getPadding().withKind(cd.getPadding().getKind().withPrefix(indent)); + } + maybeAddImport(IMPORT_FQN); + } + } + return super.visitClassDeclaration(cd, p); + } + + @Override + public Annotation visitAnnotation(Annotation annotation, ExecutionContext p) { + Annotation an = super.visitAnnotation(annotation, p); + FullyQualified anType = TypeUtils.asFullyQualified(an.getType()); + if (anType != null && IMPORT_FQN.equals(anType.getFullyQualifiedName())) { + Cursor parentCursor = getCursor().getParentTreeCursor(); + if (parentCursor != null && parentCursor.getValue() instanceof J.ClassDeclaration cd + && cd.getType() != null && configBeanFqn.equals(cd.getType().getFullyQualifiedName())) { + List currentArgs = an.getArguments(); + if (currentArgs == null || currentArgs.isEmpty() || currentArgs.get(0) instanceof J.Empty) { + an = an.withArguments(List.of(createBeanRegistrarClassFieldAccess(beanRegistrarType))); + maybeAddImport(beanRegistrarType); + } else { + List newArgs = ListUtils.map(currentArgs, (i, arg) -> { + if (arg instanceof J.FieldAccess fa) { + if (!isBeanRegistrar(fa)) { + // Turn into array + NewArray array = new J.NewArray(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null, List.of(), null, null); + array = array.withInitializer(List.of( + arg, + createBeanRegistrarClassFieldAccess(beanRegistrarType) + )); + maybeAddImport(beanRegistrarType); + return autoFormat(array, p, getCursor()); + } + } else if (arg instanceof J.NewArray na) { + List cna = new ArrayList<>(na.getInitializer()); + boolean hasBeanRegistarar = cna.stream().filter(J.FieldAccess.class::isInstance) + .map(J.FieldAccess.class::cast) + .anyMatch(ImportBeanRegistrarInConfigRecipe.this::isBeanRegistrar); + // Not found bean registrar - add it to the array + if (!hasBeanRegistarar) { + cna.add(createBeanRegistrarClassFieldAccess(beanRegistrarType)); + maybeAddImport(beanRegistrarType); + return autoFormat(na.withInitializer(cna), p, getCursor()); + } + } else if (arg instanceof J.Assignment assign && assign.getVariable() instanceof J.Identifier ident && "value".equals(ident.getSimpleName())) { + if (assign.getAssignment() instanceof J.FieldAccess fa) { + if (!isBeanRegistrar(fa)) { + // Turn into array + NewArray array = new J.NewArray(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null, List.of(), null, null); + array = array.withInitializer(List.of( + fa, + createBeanRegistrarClassFieldAccess(beanRegistrarType) + )); + maybeAddImport(beanRegistrarType); + return autoFormat(assign.withAssignment(array), p, getCursor()); + } + } else if (assign.getAssignment() instanceof J.NewArray na) { + List cna = new ArrayList<>(na.getInitializer()); + boolean hasBeanRegistarar = cna.stream().filter(J.FieldAccess.class::isInstance) + .map(J.FieldAccess.class::cast) + .anyMatch(ImportBeanRegistrarInConfigRecipe.this::isBeanRegistrar); + // Not found bean registrar - add it to the array + if (!hasBeanRegistarar) { + cna.add(createBeanRegistrarClassFieldAccess(beanRegistrarType)); + maybeAddImport(beanRegistrarType); + return autoFormat(assign.withAssignment(na.withInitializer(cna)), p, getCursor()); + } + } + } + return arg; + }); + if (newArgs != currentArgs) { + an = an.withArguments(newArgs); + } + } + } + } + return an; + } + + }); + } + + private boolean isBeanRegistrar(J.FieldAccess fa) { + if ("class".equals(fa.getSimpleName()) && fa.getTarget() instanceof TypeTree tt) { + FullyQualified t = TypeUtils.asFullyQualified(tt.getType()); + if (t != null && beanRegFqn.equals(t.getFullyQualifiedName())) { + return true; + } + } + return false; + } + + private J.FieldAccess createBeanRegistrarClassFieldAccess(JavaType.FullyQualified t) { + J.Identifier i = new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, List.of(), t.getClassName(), t, null); + JavaType.Parameterized classType = new JavaType.Parameterized(0, JavaType.ShallowClass.build("java.lang.Class"), List.of(t)); + J.Identifier c = new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, List.of(), "class", classType, null); + return new J.FieldAccess(Tree.randomId(), Space.EMPTY, Markers.EMPTY, i, JLeftPadded.build(c), classType); + } + +} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipeTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipeTest.java new file mode 100644 index 000000000..75bbd8175 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ImportBeanRegistrarInConfigRecipeTest.java @@ -0,0 +1,192 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.rewrite.java; + +import org.junit.jupiter.api.Test; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +public class ImportBeanRegistrarInConfigRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ImportBeanRegistrarInConfigRecipe("com.example.test.Config", "java.util.Date")) + .parser(JavaParser.fromJavaVersion().classpath("spring-beans", "spring-context")); + } + + @Test + void addImportAnnotation() { + rewriteRun( + java( + """ + package com.example.test; + + class Config { + } + """, + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + import java.util.Date; + + @Import(Date.class) + class Config { + } + """ + ) + ); + } + + @Test + void addImportAnnotationNextToExistingAnnotation() { + rewriteRun( + java( + """ + package com.example.test; + + @Deprecated + class Config { + } + """, + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + import java.util.Date; + + @Deprecated + @Import(Date.class) + class Config { + } + """ + ) + ); + } + + @Test + void turnIntoArray() { + rewriteRun( + java( + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + @Import(String.class) + class Config { + } + """, + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + import java.util.Date; + + @Import({String.class, Date.class}) + class Config { + } + """ + ) + ); + } + + @Test + void addEntryToArray() { + rewriteRun( + java( + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + @Import({String.class}) + class Config { + } + """, + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + import java.util.Date; + + @Import({String.class, Date.class}) + class Config { + } + """ + ) + ); + } + + @Test + void turnIntoArray_Value() { + rewriteRun( + java( + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + @Import(value = String.class) + class Config { + } + """, + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + import java.util.Date; + + @Import(value = {String.class, Date.class}) + class Config { + } + """ + ) + ); + } + + @Test + void addEntryIntoArray_Value() { + rewriteRun( + java( + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + @Import(value = {String.class}) + class Config { + } + """, + """ + package com.example.test; + + import org.springframework.context.annotation.Import; + + import java.util.Date; + + @Import(value = {String.class, Date.class}) + class Config { + } + """ + ) + ); + } + +} 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 ad3e1d072..c25f2ae9b 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 @@ -35,6 +35,7 @@ import org.springframework.ide.vscode.boot.java.reconcilers.AuthorizeHttpRequest 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.BeanRegistrarDeclarationReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.EntityIdForRepoReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.HttpSecurityLambdaDslReconciler; @@ -68,6 +69,11 @@ public class JdtConfig { return new AddConfigurationIfBeansPresentReconciler(server.getQuickfixRegistry(), springIndex); } + @Bean BeanRegistrarDeclarationReconciler beanRegistrarDeclarationReconciler(SimpleLanguageServer server, + SpringMetamodelIndex springIndex) { + return new BeanRegistrarDeclarationReconciler(server.getQuickfixRegistry(), springIndex); + } + @Bean AutowiredFieldIntoConstructorParameterReconciler autowiredFieldIntoConstructorParameterReconciler(SimpleLanguageServer server) { return new AutowiredFieldIntoConstructorParameterReconciler(server.getQuickfixRegistry()); } @@ -179,5 +185,5 @@ public class JdtConfig { @Bean JdtCronReconciler jdtCronReconciler(CronReconciler cronReconciler) { return new JdtCronReconciler(cronReconciler); } - + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java index b5eebd1a5..b2ad948a5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/common/SpringProblemCategories.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022, 2024 VMware, Inc. + * Copyright (c) 2022, 2025 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 @@ -25,6 +25,9 @@ public class SpringProblemCategories { public static final ProblemCategory BOOT_3 = new ProblemCategory("boot3", "Boot 3.x Best Practices & Optimizations", new Toggle("Enablement", EnumSet.allOf(Toggle.Option.class), AUTO, "boot-java.validation.java.boot3")); + public static final ProblemCategory BOOT_4 = new ProblemCategory("boot4", "Boot 4.x Best Practices & Optimizations", + new Toggle("Enablement", EnumSet.allOf(Toggle.Option.class), AUTO, "boot-java.validation.java.boot4")); + public static final ProblemCategory SPRING_AOT = new ProblemCategory("spring-aot", "AOT Optimizations", new Toggle("Enablement", EnumSet.of(OFF, ON), OFF, "boot-java.validation.java.spring-aot")); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot4JavaProblemType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot4JavaProblemType.java new file mode 100644 index 000000000..701f00ec1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Boot4JavaProblemType.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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; + +import static org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity.WARNING; + +import java.util.List; + +import org.eclipse.lsp4j.DiagnosticTag; +import org.springframework.ide.vscode.boot.common.SpringProblemCategories; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemCategory; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemSeverity; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; + +public enum Boot4JavaProblemType implements ProblemType { + + REGISTRAR_BEAN_DECLARATION(WARNING, "Bean derived from BeanRegistrar should be registered via `@Import` over configuration bean", "Not registered via `@Import` in a configuration bean"); + + private final ProblemSeverity defaultSeverity; + private final String description; + private String label; + private final List tags; + + private Boot4JavaProblemType(ProblemSeverity defaultSeverity, String description, String label, List tags) { + this.description = description; + this.defaultSeverity = defaultSeverity; + this.label = label; + this.tags = tags; + } + + private Boot4JavaProblemType(ProblemSeverity defaultSeverity, String description, String label) { + this(defaultSeverity, description, label, null); + } + + private Boot4JavaProblemType(ProblemSeverity defaultSeverity, String description) { + this(defaultSeverity, description, null); + } + + @Override + public ProblemSeverity getDefaultSeverity() { + return defaultSeverity; + } + + public String getLabel() { + if (label == null) { + label = createDefaultLabel(); + } + return label; + } + + @Override + public String getDescription() { + return description; + } + + private String createDefaultLabel() { + String label = this.toString().substring(5).toLowerCase().replace('_', ' '); + return Character.toUpperCase(label.charAt(0)) + label.substring(1); + } + + @Override + public String getCode() { + return name(); + } + + @Override + public ProblemCategory getCategory() { + return SpringProblemCategories.BOOT_4; + } + + @Override + public List getTags() { + return tags; + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index cdf095c5b..622e1673d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -335,7 +335,7 @@ public class ComponentSymbolProvider implements SymbolProvider { ITypeBinding typeBinding = typeDeclaration.resolveBinding(); if (typeBinding == null) return; - ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(typeDeclaration, doc, typeBinding, Set.of(Annotations.APPLICATION_LISTENER)); + ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(typeBinding, Set.of(Annotations.APPLICATION_LISTENER)); if (inTypeHierarchy == null) return; MethodDeclaration handleEventMethod = findHandleEventMethod(typeDeclaration); @@ -406,7 +406,7 @@ public class ComponentSymbolProvider implements SymbolProvider { ITypeBinding typeBinding = typeDeclaration.resolveBinding(); if (typeBinding == null) return; - ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(typeDeclaration, doc, typeBinding, Set.of(Annotations.BEAN_REGISTRAR_INTERFACE)); + ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(typeBinding, Set.of(Annotations.BEAN_REGISTRAR_INTERFACE)); if (inTypeHierarchy == null) return; MethodDeclaration registerMethod = findRegisterMethod(typeDeclaration, inTypeHierarchy); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java new file mode 100644 index 000000000..4bb86f83a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanRegistrarDeclarationReconciler.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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.TypeDeclaration; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.Boot4JavaProblemType; +import org.springframework.ide.vscode.commons.Version; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +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; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; +import org.springframework.ide.vscode.commons.rewrite.java.ImportBeanRegistrarInConfigRecipe; + +public class BeanRegistrarDeclarationReconciler implements JdtAstReconciler { + + private final QuickfixRegistry registry; + private final SpringMetamodelIndex springIndex; + + public BeanRegistrarDeclarationReconciler(QuickfixRegistry registry, SpringMetamodelIndex springIndex) { + this.registry = registry; + this.springIndex = springIndex; + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, "spring-context"); + return version != null && version.compareTo(new Version(7, 0, 0, null)) >= 0; + } + + @Override + public ProblemType getProblemType() { + return Boot4JavaProblemType.REGISTRAR_BEAN_DECLARATION; + } + + @Override + public ASTVisitor createVisitor(IJavaProject project, URI docURI, CompilationUnit cu, + IProblemCollector problemCollector, boolean isCompleteAst) { + return new ASTVisitor() { + + @Override + public boolean visit(TypeDeclaration node) { + ITypeBinding type = node.resolveBinding(); + if (type != null) { + List configBeans = new ArrayList<>(); + Path p = Path.of(docURI); + List sourceFolders = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(f -> p.startsWith(f)).collect(Collectors.toList()); + + for (Bean b : springIndex.getBeansOfProject(project.getElementName())) { +// if (b.getType().equals(type.getQualifiedName())) { +// return true; +// } + if (b.isConfiguration() && b.getLocation() != null) { + Path configBeanPath = Path.of(URI.create(b.getLocation().getUri())); + if (sourceFolders.stream().anyMatch(configBeanPath::startsWith)) { + configBeans.add(b); + } + } + } + + if (configBeans.isEmpty() || !isImportedBeanRegistrarInConfig(configBeans, type)) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), "Bean not registered", node.getName().getStartPosition(), node.getName().getLength()); + List fixes = configBeans.stream() + .filter(b -> b.getLocation() != null && b.getLocation().getUri() != null) + .map(b -> new FixDescriptor(ImportBeanRegistrarInConfigRecipe.class.getName(), List.of(b.getLocation().getUri()), "Add %s to `@Import` in %s".formatted(type.getName(), b.getName())) + .withParameters(Map.of( + "configBeanFqn", b.getType(), + "beanRegFqn", type.getQualifiedName() + )) + .withRecipeScope(RecipeScope.FILE) + ).toList(); + ReconcileUtils.setRewriteFixes(registry, problem, fixes); + problemCollector.accept(problem); + } + } + return true; + } + + }; + } + + private boolean isImportedBeanRegistrarInConfig(List configBeans, ITypeBinding beanRegType) { + return configBeans.stream() + .flatMap(bean -> Arrays.stream(bean.getAnnotations())) // look into annotations on this bean definition + .filter(annotation -> Annotations.IMPORT.equals(annotation.getAnnotationType())) // look into @Import annotations only + .flatMap(annotation -> annotation.getAttributes().get("value") == null ? Stream.empty() : Arrays.stream(annotation.getAttributes().get("value"))) // look into the attribute values of "value" attribute + .anyMatch(annotationValue -> annotationValue.getName().equals(beanRegType.getQualifiedName())); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index 3a5548a5c..5f4c20ee5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -393,7 +393,7 @@ public class ASTUtils { return false; } - public static ITypeBinding findInTypeHierarchy(TypeDeclaration typeDeclaration, TextDocument doc, ITypeBinding resolvedType, Set typesToCheck) { + public static ITypeBinding findInTypeHierarchy(ITypeBinding resolvedType, Set typesToCheck) { ITypeBinding[] interfaces = resolvedType.getInterfaces(); for (ITypeBinding resolvedInterface : interfaces) { @@ -410,7 +410,7 @@ public class ASTUtils { return resolvedInterface; } else { - ITypeBinding result = findInTypeHierarchy(typeDeclaration, doc, resolvedInterface, typesToCheck); + ITypeBinding result = findInTypeHierarchy(resolvedInterface, typesToCheck); if (result != null) { return result; } @@ -419,7 +419,7 @@ public class ASTUtils { ITypeBinding superclass = resolvedType.getSuperclass(); if (superclass != null) { - return findInTypeHierarchy(typeDeclaration, doc, superclass, typesToCheck); + return findInTypeHierarchy(superclass, typesToCheck); } else { return null; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/FunctionUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/FunctionUtils.java index 46b3aa5dd..eb52c866d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/FunctionUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/FunctionUtils.java @@ -34,7 +34,7 @@ public class FunctionUtils { ITypeBinding resolvedType = typeDeclaration.resolveBinding(); if (resolvedType != null && !resolvedType.isInterface() && !ASTUtils.isAbstractClass(typeDeclaration)) { - return ASTUtils.findInTypeHierarchy(typeDeclaration, doc, resolvedType, FUNCTION_TYPES); + return ASTUtils.findInTypeHierarchy(resolvedType, FUNCTION_TYPES); } else { return null; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanRegistrarDeclarationReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanRegistrarDeclarationReconcilerTest.java new file mode 100644 index 000000000..1ad4937a1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BeanRegistrarDeclarationReconcilerTest.java @@ -0,0 +1,244 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import org.eclipse.lsp4j.Location; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.reconcilers.BeanRegistrarDeclarationReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationAttributeValue; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +public class BeanRegistrarDeclarationReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "sf7validation"; + } + + @Override + protected String getProjectName() { + return "sf7-validation"; + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Override + protected JdtAstReconciler getReconciler() { + return new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), null); + } + + @Test + void noConfigBeans() throws Throwable { + String source = """ + package com.example.demo; + + import org.springframework.beans.factory.BeanRegistrar; + import org.springframework.beans.factory.BeanRegistry; + import org.springframework.core.env.Environment; + + public class MyBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + } + + } + """; + List problems = reconcile(() -> { + SpringMetamodelIndex springIndex = new SpringMetamodelIndex(); + + BeanRegistrarDeclarationReconciler r = new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), springIndex); + + return r; + }, "A.java", source, false); + + assertEquals(1, problems.size()); + } + + @Test + void noImportOverConfig() throws Throwable { + String source = """ + package com.example.demo; + + import org.springframework.beans.factory.BeanRegistrar; + import org.springframework.beans.factory.BeanRegistry; + import org.springframework.core.env.Environment; + + public class MyBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + } + + } + """; + List problems = reconcile(() -> { + SpringMetamodelIndex springIndex = new SpringMetamodelIndex(); + + AnnotationMetadata annotationMetadata = new AnnotationMetadata(Annotations.CONFIGURATION, false, null, Map.of()); + AnnotationMetadata[] annotations = new AnnotationMetadata[] {annotationMetadata}; + Bean configBean = new Bean("conf", "com.example.demo.Conf", null, null, null, annotations, true, "symbolLabel"); + Bean[] beans = new Bean[] {configBean}; + springIndex.updateBeans(getProjectName(), beans); + + BeanRegistrarDeclarationReconciler r = new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), springIndex); + + return r; + }, "A.java", source, false); + + assertEquals(1, problems.size()); + } + + @Test + void importOverConfig() throws Throwable { + String source = """ + package com.example.demo; + + import org.springframework.beans.factory.BeanRegistrar; + import org.springframework.beans.factory.BeanRegistry; + import org.springframework.core.env.Environment; + + public class MyBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + } + + } + """; + List problems = reconcile(() -> { + SpringMetamodelIndex springIndex = new SpringMetamodelIndex(); + + Location l = new Location(); + Path sourceFolder = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(p -> p.endsWith(Path.of("src", "main", "java"))).findFirst().orElseThrow(); + l.setUri(sourceFolder.resolve("com/example/demo/Conf.java").toUri().toASCIIString()); + + AnnotationMetadata annotationMetadata = new AnnotationMetadata(Annotations.CONFIGURATION, false, null, Map.of()); + AnnotationMetadata importMetadata = new AnnotationMetadata(Annotations.IMPORT, false, null, Map.of()); + AnnotationMetadata[] annotations = new AnnotationMetadata[] {annotationMetadata, importMetadata}; + Bean configBean = new Bean("conf", "com.example.demo.Conf", l, null, null, annotations, true, "symbolLabel"); + Bean[] beans = new Bean[] {configBean}; + springIndex.updateBeans(getProjectName(), beans); + + BeanRegistrarDeclarationReconciler r = new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), springIndex); + + return r; + }, "A.java", source, false); + + assertEquals(1, problems.size()); + } + + @Test + void imporWithBeanRegistrar() throws Throwable { + String source = """ + package com.example.demo; + + import org.springframework.beans.factory.BeanRegistrar; + import org.springframework.beans.factory.BeanRegistry; + import org.springframework.core.env.Environment; + + public class MyBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + } + + } + """; + List problems = reconcile(() -> { + SpringMetamodelIndex springIndex = new SpringMetamodelIndex(); + + Location l = new Location(); + Path sourceFolder = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(p -> p.endsWith(Path.of("src", "main", "java"))).findFirst().orElseThrow(); + l.setUri(sourceFolder.resolve("com/example/demo/B.java").toUri().toASCIIString()); + + AnnotationMetadata annotationMetadata = new AnnotationMetadata(Annotations.CONFIGURATION, false, null, Map.of()); + AnnotationMetadata importMetadata = new AnnotationMetadata(Annotations.IMPORT, false, null, + Map.of("value", new AnnotationAttributeValue[] { + new AnnotationAttributeValue("com.example.demo.MyBeanRegistrar", null) })); + AnnotationMetadata[] annotations = new AnnotationMetadata[] {annotationMetadata, importMetadata}; + Bean configBean = new Bean("conf", "com.example.demo.Conf", l, null, null, annotations, true, "symbolLabel"); + Bean[] beans = new Bean[] {configBean}; + springIndex.updateBeans(getProjectName(), beans); + + BeanRegistrarDeclarationReconciler r = new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), springIndex); + + return r; + }, "A.java", source, false); + + assertEquals(0, problems.size()); + } + + + @Test + void testConfigWithProperImport() throws Throwable { + String source = """ + package com.example.demo; + + import org.springframework.beans.factory.BeanRegistrar; + import org.springframework.beans.factory.BeanRegistry; + import org.springframework.core.env.Environment; + + public class MyBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + } + + } + """; + List problems = reconcile(() -> { + SpringMetamodelIndex springIndex = new SpringMetamodelIndex(); + + Location l = new Location(); + Path sourceFolder = IClasspathUtil.getSourceFolders(project.getClasspath()).map(f -> f.toPath()).filter(p -> p.endsWith(Path.of("src", "test", "java"))).findFirst().orElseThrow(); + l.setUri(sourceFolder.resolve("com/example/demo/Conf.java").toUri().toASCIIString()); + + AnnotationMetadata annotationMetadata = new AnnotationMetadata(Annotations.CONFIGURATION, false, null, Map.of()); + AnnotationMetadata importMetadata = new AnnotationMetadata(Annotations.IMPORT, false, null, + Map.of("value", new AnnotationAttributeValue[] { + new AnnotationAttributeValue("com.example.demo.MyBeanRegistrar", null) })); + AnnotationMetadata[] annotations = new AnnotationMetadata[] {annotationMetadata, importMetadata}; + Bean configBean = new Bean("conf", "com.example.demo.Conf", l, null, null, annotations, true, "symbolLabel"); + Bean[] beans = new Bean[] {configBean}; + springIndex.updateBeans(getProjectName(), beans); + + BeanRegistrarDeclarationReconciler r = new BeanRegistrarDeclarationReconciler(new QuickfixRegistry(), springIndex); + + return r; + }, "A.java", source, false); + + assertEquals(1, problems.size()); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/.mvn/wrapper/maven-wrapper.properties b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..d58dfb70b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw new file mode 100644 index 000000000..19529ddf8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw.cmd b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw.cmd new file mode 100644 index 000000000..249bdf382 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml new file mode 100644 index 000000000..0bc5b8a88 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.0-M2 + + + com.example + sf7-validation + 0.0.1-SNAPSHOT + sf7-validation + Demo project for Spring Boot + + + + + + + + + + + + + + + 21 + + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework + spring-core + 7.0.0-SNAPSHOT + + + org.springframework + spring-context + 7.0.0-SNAPSHOT + + + org.springframework + spring-expression + 7.0.0-SNAPSHOT + + + org.springframework + spring-beans + 7.0.0-SNAPSHOT + + + org.springframework + spring-web + 7.0.0-SNAPSHOT + + + org.springframework + spring-webmvc + 7.0.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Config.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Config.java new file mode 100644 index 000000000..695ca10aa --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Config.java @@ -0,0 +1,8 @@ +package com.example.demo; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class Config { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Sf7ValidationApplication.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Sf7ValidationApplication.java new file mode 100644 index 000000000..3b3f47880 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/java/com/example/demo/Sf7ValidationApplication.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Sf7ValidationApplication { + + public static void main(String[] args) { + SpringApplication.run(Sf7ValidationApplication.class, args); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/resources/application.properties b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/resources/application.properties new file mode 100644 index 000000000..4591d99ff --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=sf7-validation diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/test/java/com/example/demo/Sf7ValidationApplicationTests.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/test/java/com/example/demo/Sf7ValidationApplicationTests.java new file mode 100644 index 000000000..77b45b39c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/src/test/java/com/example/demo/Sf7ValidationApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class Sf7ValidationApplicationTests { + + @Test + void contextLoads() { + } + +}