diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/AnnotationMetadata.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/AnnotationMetadata.java new file mode 100644 index 000000000..a04a79c7a --- /dev/null +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/AnnotationMetadata.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.protocol.spring; + +import java.util.Map; + +/** + * @author Martin Lippert + */ +public class AnnotationMetadata { + + private final String annotationType; + private final boolean isMetaAnnotation; + private final Map attributes; + + public AnnotationMetadata(String annotationType, boolean isMetaAnnotation, Map attributes) { + this.annotationType = annotationType; + this.isMetaAnnotation = isMetaAnnotation; + this.attributes = attributes; + } + + public String getAnnotationType() { + return annotationType; + } + + public boolean isMetaAnnotation() { + return isMetaAnnotation; + } + + public Map getAttributes() { + return attributes; + } + +} diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java index 4a934aab1..67b7c6f4a 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/Bean.java @@ -23,14 +23,13 @@ public class Bean { private final Location location; private final InjectionPoint[] injectionPoints; private final Set supertypes; - private final String[] annotations; + private final AnnotationMetadata[] annotations; - public Bean(String name, String type, Location location, InjectionPoint[] injectionPoints, Set supertypes, String[] annotations) { + public Bean(String name, String type, Location location, InjectionPoint[] injectionPoints, Set supertypes, AnnotationMetadata[] annotations) { this.name = name; this.type = type; this.location = location; - this.annotations = annotations; - + if (injectionPoints != null && injectionPoints.length == 0) { this.injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS; } @@ -48,6 +47,12 @@ public class Bean { this.supertypes = supertypes; } + if (annotations != null && annotations.length == 0) { + this.annotations = DefaultValues.EMPTY_ANNOTATIONS; + } + else { + this.annotations = annotations; + } } public String getName() { @@ -70,7 +75,7 @@ public class Bean { return type != null && ((this.type != null && this.type.equals(type)) || (supertypes.contains(type))); } - public String[] getAnnotations() { + public AnnotationMetadata[] getAnnotations() { return annotations; } diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/DefaultValues.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/DefaultValues.java index 1a1588775..316098d0a 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/DefaultValues.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/DefaultValues.java @@ -17,7 +17,6 @@ public class DefaultValues { public static final Set EMPTY_SUPERTYPES = new HashSet<>(); public static final Set OBJECT_SUPERTYPE = Set.of("java.lang.Object"); - + public static final AnnotationMetadata[] EMPTY_ANNOTATIONS = new AnnotationMetadata[0]; public static final InjectionPoint[] EMPTY_INJECTION_POINTS = new InjectionPoint[0]; - } diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java index fdf64a980..57598faab 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/spring/InjectionPoint.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 2024 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 @@ -12,17 +12,29 @@ package org.springframework.ide.vscode.commons.protocol.spring; import org.eclipse.lsp4j.Location; +/** + * @author Martin Lippert + */ public class InjectionPoint { private final String name; private final String type; private final Location location; + private final AnnotationMetadata[] annotations; - public InjectionPoint(String name, String type, Location location) { + public InjectionPoint(String name, String type, Location location, AnnotationMetadata[] annotations) { super(); + this.name = name; this.type = type; this.location = location; + + if (annotations == null || (annotations != null && annotations.length == 0)) { + this.annotations = DefaultValues.EMPTY_ANNOTATIONS; + } + else { + this.annotations = annotations; + } } public String getName() { @@ -36,5 +48,9 @@ public class InjectionPoint { public Location getLocation() { return location; } + + public AnnotationMetadata[] getAnnotations() { + return annotations; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java index c95677550..229b4b9db 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java @@ -25,6 +25,7 @@ import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor; +import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProcessor; import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; @@ -113,6 +114,7 @@ public class BootJavaCompletionEngineConfigurer { providers.put(Annotations.SCOPE, new ScopeCompletionProcessor()); providers.put(Annotations.VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties)); providers.put(Annotations.DEPENDS_ON, new DependsOnCompletionProcessor(javaProjectFinder, springIndex)); + providers.put(Annotations.QUALIFIER, new QualifierCompletionProcessor(javaProjectFinder, springIndex)); providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor()); return new BootJavaCompletionEngine(cuCache, providers, snippetManager); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index fda0adf5c..3d6573365 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -46,6 +46,7 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDisc; import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider; +import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler; @@ -393,7 +394,10 @@ public class BootLanguageServerBootApp { @Bean JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) { - return new JavaDefinitionHandler(cuCache, projectFinder, List.of(new PropertyValueAnnotationDefProvider(), new DependsOnDefinitionProvider(springIndex))); + return new JavaDefinitionHandler(cuCache, projectFinder, List.of( + new PropertyValueAnnotationDefProvider(), + new DependsOnDefinitionProvider(springIndex), + new QualifierDefinitionProvider(springIndex))); } @Bean diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDisc.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDisc.java index 10ecf148e..8005cbb8f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDisc.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDisc.java @@ -35,7 +35,9 @@ import org.eclipse.lsp4j.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; import org.springframework.ide.vscode.commons.util.UriUtil; @@ -351,6 +353,7 @@ public class IndexCacheOnDisc implements IndexCache { return new GsonBuilder() .registerTypeAdapter(SymbolAddOnInformation.class, new SymbolAddOnInformationAdapter()) .registerTypeAdapter(Bean.class, new BeanJsonAdapter()) + .registerTypeAdapter(InjectionPoint.class, new InjectionPointJsonAdapter()) .registerTypeAdapter(IndexCacheStore.class, new IndexCacheStoreAdapter()) .create(); } @@ -472,10 +475,29 @@ public class IndexCacheOnDisc implements IndexCache { Set supertypes = context.deserialize(supertypesObject, Set.class); JsonElement annotationsObject = parsedObject.get("annotations"); - String[] annotations = annotationsObject == null? new String[0] : context.deserialize(annotationsObject, String[].class); + AnnotationMetadata[] annotations = annotationsObject == null ? DefaultValues.EMPTY_ANNOTATIONS : context.deserialize(annotationsObject, AnnotationMetadata[].class); return new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations); } } + private static class InjectionPointJsonAdapter implements JsonDeserializer { + + @Override + public InjectionPoint deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject parsedObject = json.getAsJsonObject(); + + String injectionPointName = parsedObject.get("name").getAsString(); + String injectionPointType = parsedObject.get("type").getAsString(); + + JsonElement locationObject = parsedObject.get("location"); + Location location = context.deserialize(locationObject, Location.class); + + JsonElement annotationsObject = parsedObject.get("annotations"); + AnnotationMetadata[] annotations = annotationsObject == null ? DefaultValues.EMPTY_ANNOTATIONS : context.deserialize(annotationsObject, AnnotationMetadata[].class); + + return new InjectionPoint(injectionPointName, injectionPointType, location, annotations); + } + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java index 84707ee7e..4956d3099 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java @@ -32,7 +32,6 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.Annotations; -import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.handlers.AbstractSymbolProvider; import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; @@ -40,6 +39,7 @@ import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; import org.springframework.ide.vscode.boot.java.utils.FunctionUtils; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; import org.springframework.ide.vscode.commons.util.BadLocationException; @@ -95,9 +95,16 @@ public class BeansSymbolProvider extends AbstractSymbolProvider { Set supertypes = new HashSet<>(); ASTUtils.findSupertypes(beanType, supertypes); - String[] annotations = AnnotationHierarchies - .findTransitiveSuperAnnotationBindings(node.resolveAnnotationBinding()) - .map(t -> t.getAnnotationType().getQualifiedName()).toArray(String[]::new); + Collection annotationsOnMethod = ASTUtils.getAnnotations(method); + AnnotationMetadata[] annotations = annotationsOnMethod.stream() + .map(an -> an.resolveAnnotationBinding()) + .map(t -> new AnnotationMetadata(t.getAnnotationType().getQualifiedName(), false, ASTUtils.getAttributes(t))) + .toArray(AnnotationMetadata[]::new); + +// AnnotationMetadata[] annotations = AnnotationHierarchies +// .findTransitiveSuperAnnotationBindings(node.resolveAnnotationBinding()) +// .map(t -> new AnnotationMetadata(t.getAnnotationType().getQualifiedName(), false, getAttributes(t))) +// .toArray(AnnotationMetadata[]::new); Bean beanDefinition = new Bean(nameAndRegion.getT1(), beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations); 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 6f3c40875..c0de66626 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 @@ -34,6 +34,7 @@ import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; import org.springframework.ide.vscode.commons.util.BadLocationException; @@ -66,6 +67,7 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider { protected Tuple.Two createSymbol(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, TextDocument doc) throws BadLocationException { String annotationTypeName = annotationType.getName(); + Collection metaAnnotationNames = metaAnnotations.stream() .map(ITypeBinding::getName) .collect(Collectors.toList()); @@ -94,8 +96,17 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider { Set supertypes = new HashSet<>(); ASTUtils.findSupertypes(beanType, supertypes); - String[] annotations = Stream.concat(Stream.of(annotationType), metaAnnotations.stream()).map(t -> t.getQualifiedName()).toArray(String[]::new); - + Collection annotationsOnType = ASTUtils.getAnnotations(type); + + AnnotationMetadata[] annotations = Stream.concat( + annotationsOnType.stream() + .map(an -> an.resolveAnnotationBinding()) + .map(t -> new AnnotationMetadata(t.getAnnotationType().getQualifiedName(), false, ASTUtils.getAttributes(t))) + , + metaAnnotations.stream() + .map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null))) + .toArray(AnnotationMetadata[]::new); + Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations); return Tuple.two(new EnhancedSymbolInformation(symbol, addon), beanDefinition); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java index db8fb08d1..b824295f4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java @@ -40,6 +40,7 @@ import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; import org.springframework.ide.vscode.commons.util.BadLocationException; @@ -67,8 +68,7 @@ public class FeignClientSymbolProvider extends AbstractSymbolProvider { } } - private Two createSymbol(Annotation node, ITypeBinding annotationType, - Collection metaAnnotations, TextDocument doc) throws BadLocationException { + private Two createSymbol(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, TextDocument doc) throws BadLocationException { String annotationTypeName = annotationType.getName(); Collection metaAnnotationNames = metaAnnotations.stream() .map(ITypeBinding::getName) @@ -92,8 +92,17 @@ public class FeignClientSymbolProvider extends AbstractSymbolProvider { Set supertypes = new HashSet<>(); ASTUtils.findSupertypes(beanType, supertypes); - String[] annotations = Stream.concat(Stream.of(annotationType), metaAnnotations.stream()).map(t -> t.getQualifiedName()).toArray(String[]::new); - + Collection annotationsOnType = ASTUtils.getAnnotations(type); + + AnnotationMetadata[] annotations = Stream.concat( + annotationsOnType.stream() + .map(an -> an.resolveAnnotationBinding()) + .map(t -> new AnnotationMetadata(t.getAnnotationType().getQualifiedName(), false, ASTUtils.getAttributes(t))) + , + metaAnnotations.stream() + .map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null))) + .toArray(AnnotationMetadata[]::new); + Bean beanDefinition = new Bean(beanName, beanType == null ? "" : beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations); return Tuple.two(new EnhancedSymbolInformation(symbol, addon), beanDefinition); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProcessor.java new file mode 100644 index 000000000..bb2b52e09 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProcessor.java @@ -0,0 +1,275 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ArrayInitializer; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * @author Martin Lippert + */ +public class QualifierCompletionProcessor implements CompletionProvider { + + private final JavaProjectFinder projectFinder; + private final SpringMetamodelIndex springIndex; + + public QualifierCompletionProcessor(JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) { + this.projectFinder = projectFinder; + this.springIndex = springIndex; + } + + @Override + public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc, Collection completions) { + + Optional optionalProject = projectFinder.find(doc.getId()); + if (!optionalProject.isPresent()) { + return; + } + + IJavaProject project = optionalProject.get(); + + try { + + // case: @Qualifier(<*>) + if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) { + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + + for (Bean bean : beans) { + + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(offset, offset, "\"" + bean.getName() + "\""); + + // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet + // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string + // if it cannot resolve it. If sending this as plain text, then insertion happens correctly + QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); + + completions.add(proposal); + } + } + // case: @Qualifier(prefix<*>) + else if (node instanceof SimpleName && node.getParent() instanceof Annotation) { + computeProposalsForSimpleName(project, node, completions, offset, doc); + } + // case: @Qualifier(value=<*>) + else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair + && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { + computeProposalsForSimpleName(project, node, completions, offset, doc); + } + // case: @Qualifier("prefix<*>") + else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + computeProposalsForStringLiteral(project, node, completions, offset, doc); + } + } + else if (node instanceof StringLiteral && node.getParent() instanceof ArrayInitializer) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + computeProposalsForInsideArrayInitializer(project, node, completions, offset, doc); + } + } + // case: @Qualifier(value="prefix<*>") + else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair + && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + computeProposalsForStringLiteral(project, node, completions, offset, doc); + } + } + // case: @Qualifier({<*>}) + else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotation) { + computeProposalsForArrayInitializr(project, (ArrayInitializer) node, completions, offset, doc); + } + } + catch (Exception e) { + e.printStackTrace(); + } + } + + private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection completions, int offset, IDocument doc) { + String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition()); + + int startOffset = node.getStartPosition(); + int endOffset = node.getStartPosition() + node.getLength(); + + String proposalPrefix = "\""; + String proposalPostfix = "\""; + + Set mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + List matchingBeans = Arrays.stream(beans) + .filter(bean -> bean.getName().toLowerCase().startsWith(prefix.toLowerCase())) + .filter(bean -> !mentionedBeans.contains(bean.getName())) + .collect(Collectors.toList()); + + for (Bean bean : matchingBeans) { + + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(startOffset, endOffset, proposalPrefix + bean.getName() + proposalPostfix); + + // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet + // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string + // if it cannot resolve it. If sending this as plain text, then insertion happens correctly + QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); + + completions.add(proposal); + } + } + + private void computeProposalsForStringLiteral(IJavaProject project, ASTNode node, Collection completions, int offset, IDocument doc) throws BadLocationException { + int length = offset - (node.getStartPosition() + 1); + + String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, length), length); + int startOffset = offset - prefix.length(); + int endOffset = offset; + + Set mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + + final String filterPrefix = prefix; + List matchingBeans = Arrays.stream(beans) + .filter(bean -> bean.getName().toLowerCase().startsWith(filterPrefix.toLowerCase())) + .filter(bean -> !mentionedBeans.contains(bean.getName())) + .collect(Collectors.toList()); + + for (Bean bean : matchingBeans) { + + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(startOffset, endOffset, bean.getName()); + + // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet + // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string + // if it cannot resolve it. If sending this as plain text, then insertion happens correctly + QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); + + completions.add(proposal); + } + } + + private void computeProposalsForArrayInitializr(IJavaProject project, ArrayInitializer node, Collection completions, int offset, IDocument doc) { + Set mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + List filteredBeans = Arrays.stream(beans) + .filter(bean -> !mentionedBeans.contains(bean.getName())) + .collect(Collectors.toList()); + + for (Bean bean : filteredBeans) { + + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(offset, offset, "\"" + bean.getName() + "\""); + + // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet + // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string + // if it cannot resolve it. If sending this as plain text, then insertion happens correctly + QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); + + completions.add(proposal); + } + } + + private void computeProposalsForInsideArrayInitializer(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument doc) throws BadLocationException { + int length = offset - (node.getStartPosition() + 1); + if (length >= 0) { + computeProposalsForStringLiteral(project, node, completions, offset, doc); + } + else { + Set mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + List filteredBeans = Arrays.stream(beans) + .filter(bean -> !mentionedBeans.contains(bean.getName())) + .collect(Collectors.toList()); + + for (Bean bean : filteredBeans) { + + DocumentEdits edits = new DocumentEdits(doc, false); + edits.replace(offset, offset, "\"" + bean.getName() + "\","); + + // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet + // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string + // if it cannot resolve it. If sending this as plain text, then insertion happens correctly + QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); + + completions.add(proposal); + } + } + } + + private String identifyPropertyPrefix(String nodeContent, int offset) { + String result = nodeContent.substring(0, offset); + + int i = offset - 1; + while (i >= 0) { + char c = nodeContent.charAt(i); + if (c == '}' || c == '{' || c == '$' || c == '#') { + result = result.substring(i + 1, offset); + break; + } + i--; + } + + return result; + } + + private Set alreadyMentionedBeans(ASTNode node) { + Set result = new HashSet<>(); + + ArrayInitializer arrayNode = null; + while (node != null && arrayNode == null && !(node instanceof Annotation)) { + if (node instanceof ArrayInitializer) { + arrayNode = (ArrayInitializer) node; + } + else { + node = node.getParent(); + } + } + + if (arrayNode != null) { + List expressions = arrayNode.expressions(); + for (Object expression : expressions) { + if (expression instanceof StringLiteral) { + StringLiteral stringExr = (StringLiteral) expression; + String value = stringExr.getLiteralValue(); + result.add(value); + } + } + } + + return result; + } + + + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProposal.java new file mode 100644 index 000000000..6eafa90e8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProposal.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans; + +import org.eclipse.lsp4j.CompletionItemKind; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; +import org.springframework.ide.vscode.commons.util.Renderable; + +/** + * @author Martin Lippert + */ +public class QualifierCompletionProposal implements ICompletionProposal { + + private static final String EMPTY_DETAIL = ""; + + private DocumentEdits edits; + private String label; + private String detail; + private Renderable documentation; + + public QualifierCompletionProposal(DocumentEdits edits, String label, String detail, Renderable documentation) { + this.edits = edits; + this.label = label; + // PT 161489998 - Detail for proposal must not be null. For some clients like Eclipse, + // a null detail results in an NPE at JDT level when inserting the proposal in the editor, and results + // in odd behaviour like insertion of an extra new line. + this.detail = detail == null ? EMPTY_DETAIL : detail; + this.documentation = documentation; + } + + @Override + public String getLabel() { + return this.label; + } + + @Override + public CompletionItemKind getKind() { + return CompletionItemKind.Value; + } + + @Override + public DocumentEdits getTextEdit() { + return this.edits; + } + + @Override + public String getDetail() { + return this.detail; + } + + @Override + public Renderable getDocumentation() { + return this.documentation; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierDefinitionProvider.java new file mode 100644 index 000000000..cdf20c1c7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierDefinitionProvider.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +/** + * @author Martin Lippert + */ +public class QualifierDefinitionProvider implements IJavaDefinitionProvider { + + private final SpringMetamodelIndex springIndex; + + public QualifierDefinitionProvider(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n) { + if (n instanceof StringLiteral) { + StringLiteral valueNode = (StringLiteral) n; + + ASTNode parent = ASTUtils.getNearestAnnotationParent(valueNode); + + if (parent != null && parent instanceof Annotation) { + Annotation a = (Annotation) parent; + IAnnotationBinding binding = a.resolveAnnotationBinding(); + if (binding != null && binding.getAnnotationType() != null && Annotations.QUALIFIER.equals(binding.getAnnotationType().getQualifiedName())) { + String beanName = valueNode.getLiteralValue(); + + if (beanName != null && beanName.length() > 0) { + return findBeansWithName(project, beanName); + } + } + } + } + return Collections.emptyList(); + } + + private List findBeansWithName(IJavaProject project, String beanName) { + Bean[] beans = this.springIndex.getBeansWithName(project.getElementName(), beanName); + + return Arrays.stream(beans) + .map(bean -> { + return new LocationLink(bean.getLocation().getUri(), bean.getLocation().getRange(), bean.getLocation().getRange()); + }) + .collect(Collectors.toList()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java index 4c52d9a5d..500063153 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java @@ -10,9 +10,11 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.data; +import java.util.Collection; import java.util.HashSet; import java.util.Set; +import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Location; @@ -30,6 +32,7 @@ import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; import org.springframework.ide.vscode.commons.util.BadLocationException; @@ -73,7 +76,14 @@ public class DataRepositorySymbolProvider extends AbstractSymbolProvider { ASTUtils.findSupertypes(concreteBeanTypeBindung, supertypes); String concreteRepoType = concreteBeanTypeBindung.getQualifiedName(); - Bean beanDefinition = new Bean(beanName, concreteRepoType, location, injectionPoints, supertypes, new String[0]); + + Collection annotationsOnMethod = ASTUtils.getAnnotations(typeDeclaration); + AnnotationMetadata[] annotations = annotationsOnMethod.stream() + .map(an -> an.resolveAnnotationBinding()) + .map(t -> new AnnotationMetadata(t.getAnnotationType().getQualifiedName(), false, ASTUtils.getAttributes(t))) + .toArray(AnnotationMetadata[]::new); + + Bean beanDefinition = new Bean(beanName, concreteRepoType, location, injectionPoints, supertypes, annotations); context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol)); context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition)); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java index c7cfa044b..ff05a1d60 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java @@ -24,7 +24,6 @@ import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.IMemberValuePairBinding; import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.IVariableBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.NormalAnnotation; @@ -186,40 +185,17 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { IAnnotationBinding annotationBinding = getAnnotationFromSupertypes(node, context); IMemberValuePairBinding valuePair = getValuePair(annotationBinding, attributeNames); - if (valuePair != null) { - Object value = valuePair.getValue(); - if (value instanceof Object[]) { - Object[] values = (Object[]) value; - String[] result = new String[values.length]; - for (int k = 0; k < result.length; k++) { - - Object v = values[k]; - if (v instanceof IVariableBinding) { - IVariableBinding varBinding = (IVariableBinding) v; - result[k] = varBinding.getName(); - - ITypeBinding klass = varBinding.getDeclaringClass(); - if (klass!=null) { - context.addDependency(klass); - } - - } - else if (v instanceof String) { - result[k] = (String) v; - } - } - return result; + ASTUtils.MemberValuePairAndType result = ASTUtils.getValuesFromValuePair(valuePair); + if (result != null) { + if (result.dereferencedType != null) { + context.addDependency(result.dereferencedType); } - else if (value instanceof String[]) { - return (String[]) value; - } - else if (value != null) { - return new String[] {value.toString()}; - } + return result.values; + } + else { + return null; } - - return null; } private IMemberValuePairBinding getValuePair(IAnnotationBinding annotationBinding, Set names) { 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 883b14e02..3e57e6889 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 @@ -13,7 +13,9 @@ package org.springframework.ide.vscode.boot.java.utils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -26,7 +28,9 @@ import org.eclipse.jdt.core.dom.ArrayInitializer; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.IMemberValuePairBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.IVariableBinding; import org.eclipse.jdt.core.dom.MemberValuePair; @@ -47,6 +51,7 @@ import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.jdt.imports.ImportRewrite; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; import org.springframework.ide.vscode.commons.util.BadLocationException; @@ -256,8 +261,15 @@ public class ASTUtils { } - public static Collection getAnnotations(TypeDeclaration declaringType) { - Object modifiersObj = declaringType.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY); + public static Collection getAnnotations(TypeDeclaration typeDeclaration) { + return getAnnotationsFromModifiers(typeDeclaration.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY)); + } + + public static Collection getAnnotations(MethodDeclaration methodDeclaration) { + return getAnnotationsFromModifiers(methodDeclaration.getStructuralProperty(MethodDeclaration.MODIFIERS2_PROPERTY)); + } + + private static Collection getAnnotationsFromModifiers(Object modifiersObj) { if (modifiersObj instanceof List) { ImmutableList.Builder annotations = ImmutableList.builder(); for (Object node : (List)modifiersObj) { @@ -335,13 +347,17 @@ public class ASTUtils { if (object instanceof VariableDeclaration) { VariableDeclaration variable = (VariableDeclaration) object; String name = variable.getName().toString(); - String type = variable.resolveBinding().getType().getQualifiedName(); + + IVariableBinding variableBinding = variable.resolveBinding(); + String type = variableBinding.getType().getQualifiedName(); DocumentRegion region = ASTUtils.nodeRegion(doc, variable.getName()); Range range = doc.toRange(region); Location location = new Location(doc.getUri(), range); - result.add(new InjectionPoint(name, type, location)); + AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(variableBinding.getAnnotations()); + + result.add(new InjectionPoint(name, type, location, annotations)); } } return result; @@ -392,13 +408,17 @@ public class ASTUtils { if (object instanceof VariableDeclaration) { VariableDeclaration variable = (VariableDeclaration) object; String name = variable.getName().toString(); - String type = variable.resolveBinding().getType().getQualifiedName(); + + IVariableBinding variableBinding = variable.resolveBinding(); + String type = variableBinding.getType().getQualifiedName(); DocumentRegion region = ASTUtils.nodeRegion(doc, variable.getName()); Range range = doc.toRange(region); Location location = new Location(doc.getUri(), range); - result.add(new InjectionPoint(name, type, location)); + AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(variableBinding.getAnnotations()); + + result.add(new InjectionPoint(name, type, location, annotations)); } } @@ -419,11 +439,14 @@ public class ASTUtils { for (FieldDeclaration field : fields) { boolean autowiredField = false; + + List fieldAnnotations = new ArrayList<>(); List modifiers = field.modifiers(); for (Object modifier : modifiers) { if (modifier instanceof Annotation) { Annotation annotation = (Annotation) modifier; + fieldAnnotations.add(annotation); String qualifiedName = annotation.resolveTypeBinding().getQualifiedName(); if (Annotations.AUTOWIRED.equals(qualifiedName)) { @@ -445,7 +468,12 @@ public class ASTUtils { String fieldType = field.getType().resolveBinding().getQualifiedName(); - result.add(new InjectionPoint(fieldName, fieldType, fieldLocation)); + AnnotationMetadata[] annotationsMetadata = fieldAnnotations.stream() + .map(an -> an.resolveAnnotationBinding()) + .map(t -> new AnnotationMetadata(t.getAnnotationType().getQualifiedName(), false, ASTUtils.getAttributes(t))) + .toArray(AnnotationMetadata[]::new); + + result.add(new InjectionPoint(fieldName, fieldType, fieldLocation, annotationsMetadata)); } } } @@ -454,11 +482,82 @@ public class ASTUtils { return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS; } + private static AnnotationMetadata[] getAnnotationsMetadata(IAnnotationBinding[] annotations) { + return Arrays.stream(annotations) + .map(t -> new AnnotationMetadata(t.getAnnotationType().getQualifiedName(), false, getAttributes(t))) + .toArray(AnnotationMetadata[]::new); + } + + public static Map getAttributes(IAnnotationBinding t) { + Map result = new LinkedHashMap<>(); + + IMemberValuePairBinding[] pairs = t.getDeclaredMemberValuePairs(); + for (IMemberValuePairBinding pair : pairs) { + MemberValuePairAndType values = ASTUtils.getValuesFromValuePair(pair); + if (values != null) { + result.put(pair.getName(), values.values); + } + } + + return result; + } + public static ASTNode getNearestAnnotationParent(ASTNode node) { while (node != null && !(node instanceof Annotation)) { node = node.getParent(); } return node; } + + public static MemberValuePairAndType getValuesFromValuePair(IMemberValuePairBinding valuePair) { + if (valuePair != null) { + Object value = valuePair.getValue(); + + MemberValuePairAndType result = new MemberValuePairAndType(); + + if (value instanceof Object[]) { + Object[] values = (Object[]) value; + result.values = new String[values.length]; + for (int k = 0; k < values.length; k++) { + + Object v = values[k]; + if (v instanceof IVariableBinding) { + IVariableBinding varBinding = (IVariableBinding) v; + result.values[k] = varBinding.getName(); + + ITypeBinding klass = varBinding.getDeclaringClass(); + if (klass != null) { + result.dereferencedType= klass; + } + + } + else if (v instanceof String) { + result.values[k] = (String) v; + } + else if (v instanceof ITypeBinding) { + result.values[k] = ((ITypeBinding) v).getQualifiedName(); + } + else if (v != null) { + result.values[k] = v.toString(); + } + } + return result; + } + else if (value instanceof String[]) { + result.values = (String[]) value; + return result; + } + else if (value != null) { + result.values = new String[] {value.toString()}; + return result; + } + } + return null; + } + + public static class MemberValuePairAndType { + public String[] values; + public ITypeBinding dereferencedType; + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java index 3deb2c521..5d507d405 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexTest.java @@ -18,7 +18,9 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import org.assertj.core.util.Arrays; @@ -28,6 +30,7 @@ import org.eclipse.lsp4j.Range; import org.junit.jupiter.api.Test; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDisc; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; @@ -38,7 +41,9 @@ public class SpringMetamodelIndexTest { private InjectionPoint[] emptyInjectionPoints = new InjectionPoint[0]; private Set emptySupertypes = new HashSet<>(); - private String[] emptyAnnotations = new String[0]; + private AnnotationMetadata[] emptyAnnotations = new AnnotationMetadata[0]; + private AnnotationMetadata[] emptyInjectionAnnotations = new AnnotationMetadata[0]; + private Map emptyAnnotationAttributes = new LinkedHashMap<>(); private Location locationForDoc1 = new Location("docURI1", new Range(new Position(1, 1), new Position(1, 10))); private Location locationForDoc2 = new Location("docURI2", new Range(new Position(2, 1), new Position(2, 10))); @@ -220,8 +225,10 @@ public class SpringMetamodelIndexTest { @Test void testOverallSerializeDeserializeBeans() { - InjectionPoint point1 = new InjectionPoint("point1", "point1-type", locationForDoc2); - InjectionPoint point2 = new InjectionPoint("point2", "point2-type", locationForDoc1); + InjectionPoint point1 = new InjectionPoint("point1", "point1-type", locationForDoc2, new AnnotationMetadata[] + {new AnnotationMetadata("anno1", false, emptyAnnotationAttributes),new AnnotationMetadata("anno2", false, emptyAnnotationAttributes)}); + + InjectionPoint point2 = new InjectionPoint("point2", "point2-type", locationForDoc1, null); Bean bean1 = new Bean("beanName1", "beanType", locationForDoc1, new InjectionPoint[] {point1, point2}, Set.of("supertype1", "supertype2"), emptyAnnotations); String serialized = bean1.toString(); @@ -247,6 +254,12 @@ public class SpringMetamodelIndexTest { assertTrue(deserializedBean.isTypeCompatibleWith("supertype1")); assertTrue(deserializedBean.isTypeCompatibleWith("supertype2")); assertFalse(deserializedBean.isTypeCompatibleWith("java.lang.String")); + + assertEquals(2, points[0].getAnnotations().length); + assertEquals("anno1", points[0].getAnnotations()[0].getAnnotationType()); + assertEquals("anno2", points[0].getAnnotations()[1].getAnnotationType()); + + assertEquals(0, points[1].getAnnotations().length); } @Test @@ -270,6 +283,12 @@ public class SpringMetamodelIndexTest { assertSame(DefaultValues.EMPTY_INJECTION_POINTS, bean1.getInjectionPoints()); } + @Test + void testEmptyAnnotationOptimization() { + InjectionPoint point = new InjectionPoint("pointName", "pointType", locationForDoc1, emptyInjectionAnnotations); + assertSame(DefaultValues.EMPTY_ANNOTATIONS, point.getAnnotations()); + } + @Test void testFindNoMatchingBeansWithEmptySupertypes() { SpringMetamodelIndex index = new SpringMetamodelIndex(); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java index fd4a5af9c..6e60c5063 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.index.test; +import static org.junit.Assert.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -17,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -34,6 +36,7 @@ import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; @@ -90,7 +93,7 @@ public class SpringMetamodelIndexerBeansTest { Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "bean1"); String docUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString(); - Location location = new Location(docUri, new Range(new Position(13, 1), new Position(13, 6))); + Location location = new Location(docUri, new Range(new Position(15, 1), new Position(15, 6))); assertEquals(location, beans[0].getLocation()); } @@ -194,12 +197,12 @@ public class SpringMetamodelIndexerBeansTest { assertEquals("bean1", injectionPoints[0].getName()); assertEquals("org.test.BeanClass1", injectionPoints[0].getType()); - Location ip1Location = new Location(docUri, new Range(new Position(10, 31), new Position(10, 36))); + Location ip1Location = new Location(docUri, new Range(new Position(12, 20), new Position(12, 25))); assertEquals(ip1Location, injectionPoints[0].getLocation()); assertEquals("bean2", injectionPoints[1].getName()); assertEquals("org.test.BeanClass2", injectionPoints[1].getType()); - Location ip2Location = new Location(docUri, new Range(new Position(11, 31), new Position(11, 36))); + Location ip2Location = new Location(docUri, new Range(new Position(16, 20), new Position(16, 25))); assertEquals(ip2Location, injectionPoints[1].getLocation()); } @@ -208,13 +211,15 @@ public class SpringMetamodelIndexerBeansTest { Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "customerRepository"); assertEquals(1, beans.length); - assertEquals("customerRepository", beans[0].getName()); - assertEquals("org.test.springdata.CustomerRepository", beans[0].getType()); - - assertTrue(beans[0].isTypeCompatibleWith("org.test.springdata.CustomerRepository")); - assertTrue(beans[0].isTypeCompatibleWith("org.springframework.data.repository.CrudRepository")); + Bean repositoryBean = beans[0]; - InjectionPoint[] injectionPoints = beans[0].getInjectionPoints(); + assertEquals("customerRepository", repositoryBean.getName()); + assertEquals("org.test.springdata.CustomerRepository", repositoryBean.getType()); + + assertTrue(repositoryBean.isTypeCompatibleWith("org.test.springdata.CustomerRepository")); + assertTrue(repositoryBean.isTypeCompatibleWith("org.springframework.data.repository.CrudRepository")); + + InjectionPoint[] injectionPoints = repositoryBean.getInjectionPoints(); assertEquals(0, injectionPoints.length); assertSame(DefaultValues.EMPTY_INJECTION_POINTS, injectionPoints); } @@ -235,5 +240,193 @@ public class SpringMetamodelIndexerBeansTest { assertFalse(beans[0].isTypeCompatibleWith("java.lang.String")); assertFalse(beans[0].isTypeCompatibleWith("java.util.Comparator")); } + + @Test + void testAnnotationMetadataFromComponentBeans() { + Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "mainClass"); + assertEquals(1, beans.length); + + Bean mainClassBean = beans[0]; + AnnotationMetadata[] annotations = mainClassBean.getAnnotations(); + + assertEquals(4, annotations.length); + assertEquals("org.springframework.boot.autoconfigure.SpringBootApplication", annotations[0].getAnnotationType()); + assertFalse(annotations[0].isMetaAnnotation()); + + assertEquals("org.springframework.boot.SpringBootConfiguration", annotations[1].getAnnotationType()); + assertTrue(annotations[1].isMetaAnnotation()); + assertEquals("org.springframework.context.annotation.Configuration", annotations[2].getAnnotationType()); + assertTrue(annotations[2].isMetaAnnotation()); + + assertEquals("org.springframework.stereotype.Component", annotations[3].getAnnotationType()); + assertTrue(annotations[3].isMetaAnnotation()); + } + + @Test + void testAnnotationMetadataFromBeanMethodBean() { + Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "bean3"); + assertEquals(1, beans.length); + + Bean mainClassBean = beans[0]; + AnnotationMetadata[] annotations = mainClassBean.getAnnotations(); + + assertEquals(3, annotations.length); + + AnnotationMetadata beanAnnotation = annotations[0]; + assertEquals("org.springframework.context.annotation.Bean", beanAnnotation.getAnnotationType()); + assertFalse(annotations[0].isMetaAnnotation()); + assertEquals(0, annotations[0].getAttributes().size()); + + AnnotationMetadata qualifierAnnotation = annotations[1]; + assertEquals("org.springframework.beans.factory.annotation.Qualifier", qualifierAnnotation.getAnnotationType()); + Map attributes = qualifierAnnotation.getAttributes(); + assertEquals(1, attributes.size()); + assertTrue(attributes.containsKey("value")); + assertArrayEquals(new String[] {"qualifier1"}, attributes.get("value")); + + AnnotationMetadata profileAnnotation = annotations[2]; + assertEquals("org.springframework.context.annotation.Profile", profileAnnotation.getAnnotationType()); + assertFalse(profileAnnotation.isMetaAnnotation()); + + attributes = profileAnnotation.getAttributes(); + assertEquals(1, attributes.size()); + assertTrue(attributes.containsKey("value")); + assertArrayEquals(new String[] {"testprofile","testprofile2"}, attributes.get("value")); + } + + @Test + void testAnnotationMetadataFromBeanMethodWithInjectionPointAnnotations() { + Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "beanWithAnnotationsOnInjectionPoints"); + assertEquals(1, beans.length); + + Bean bean = beans[0]; + AnnotationMetadata[] annotations = bean.getAnnotations(); + + assertEquals(2, annotations.length); + + AnnotationMetadata beanAnnotation = annotations[0]; + AnnotationMetadata dependsonAnnotation = annotations[1]; + + assertEquals("org.springframework.context.annotation.Bean", beanAnnotation.getAnnotationType()); + assertEquals("org.springframework.context.annotation.DependsOn", dependsonAnnotation.getAnnotationType()); + + Map dependsOnAttributes = dependsonAnnotation.getAttributes(); + assertEquals(1, dependsOnAttributes.size()); + assertArrayEquals(new String[] {"bean1", "bean2"}, dependsOnAttributes.get("value")); + + InjectionPoint[] injectionPoints = bean.getInjectionPoints(); + assertEquals(2, injectionPoints.length); + + AnnotationMetadata[] annotationsFromPoint1 = injectionPoints[0].getAnnotations(); + AnnotationMetadata[] annotationsFromPoint2 = injectionPoints[1].getAnnotations(); + + assertEquals(1, annotationsFromPoint1.length); + assertEquals(1, annotationsFromPoint2.length); + + assertEquals("org.springframework.beans.factory.annotation.Qualifier", annotationsFromPoint1[0].getAnnotationType()); + Map attributesFromParam1 = annotationsFromPoint1[0].getAttributes(); + assertEquals(1, attributesFromParam1.size()); + assertArrayEquals(new String[] {"q1"}, attributesFromParam1.get("value")); + + assertEquals("org.springframework.beans.factory.annotation.Qualifier", annotationsFromPoint2[0].getAnnotationType()); + Map attributesFromParam2 = annotationsFromPoint2[0].getAttributes(); + assertEquals(1, attributesFromParam2.size()); + assertArrayEquals(new String[] {"q2"}, attributesFromParam2.get("value")); + } + + @Test + void testAnnotationMetadataFromComponentClass() { + Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "configurationWithInjectionsAndAnnotations"); + assertEquals(1, beans.length); + + Bean bean = beans[0]; + + InjectionPoint[] injectionPoints = bean.getInjectionPoints(); + assertEquals(0, injectionPoints.length); + + AnnotationMetadata[] annotations = bean.getAnnotations(); + + assertEquals(4, annotations.length); + + AnnotationMetadata configurationAnnotation= annotations[0]; + AnnotationMetadata qualifierAnnotation = annotations[1]; + AnnotationMetadata runtimeHintsAnnotation = annotations[2]; + AnnotationMetadata componentMetaAnnotation = annotations[3]; + + assertEquals("org.springframework.context.annotation.Configuration", configurationAnnotation.getAnnotationType()); + assertEquals("org.springframework.beans.factory.annotation.Qualifier", qualifierAnnotation.getAnnotationType()); + assertEquals("org.springframework.context.annotation.ImportRuntimeHints", runtimeHintsAnnotation.getAnnotationType()); + assertEquals("org.springframework.stereotype.Component", componentMetaAnnotation.getAnnotationType()); + + Map qualifierAttributes = qualifierAnnotation.getAttributes(); + assertEquals(1, qualifierAttributes.size()); + assertArrayEquals(new String[] {"qualifier"}, qualifierAttributes.get("value")); + + Map runtimeHintsAttributes = runtimeHintsAnnotation.getAttributes(); + assertEquals(1, runtimeHintsAttributes.size()); + assertArrayEquals(new String[] {"org.test.MainClass"}, runtimeHintsAttributes.get("value")); + } + + @Test + void testAnnotationMetadataFromInjectionPointsFromAutowiredFields() { + Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "autowiredInjectionService"); + assertEquals(1, beans.length); + + InjectionPoint[] injectionPoints = beans[0].getInjectionPoints(); + assertEquals(2, injectionPoints.length); + + AnnotationMetadata[] annotationsPoint1 = injectionPoints[0].getAnnotations(); + assertEquals(1, annotationsPoint1.length); + assertEquals("org.springframework.beans.factory.annotation.Autowired", annotationsPoint1[0].getAnnotationType()); + assertFalse(annotationsPoint1[0].isMetaAnnotation()); + assertEquals(0, annotationsPoint1[0].getAttributes().size()); + + AnnotationMetadata[] annotationsPoint2 = injectionPoints[1].getAnnotations(); + assertEquals(2, annotationsPoint2.length); + + AnnotationMetadata autowiredFromPoint2 = annotationsPoint2[0]; + assertEquals("org.springframework.beans.factory.annotation.Autowired", autowiredFromPoint2.getAnnotationType()); + assertFalse(autowiredFromPoint2.isMetaAnnotation()); + assertEquals(0, autowiredFromPoint2.getAttributes().size()); + + AnnotationMetadata qualifierFromPoint2 = annotationsPoint2[1]; + assertEquals("org.springframework.beans.factory.annotation.Qualifier", qualifierFromPoint2.getAnnotationType()); + assertFalse(qualifierFromPoint2.isMetaAnnotation()); + assertEquals(1, qualifierFromPoint2.getAttributes().size()); + + Map qualifierAttributes = qualifierFromPoint2.getAttributes(); + assertEquals(1, qualifierAttributes.size()); + assertArrayEquals(new String[] {"qual1"}, qualifierAttributes.get("value")); + } + + @Test + void testAnnotationMetadataFromSpringDataRepository() { + Bean[] beans = springIndex.getBeansWithName("test-spring-indexing", "customerRepository"); + + assertEquals(1, beans.length); + + AnnotationMetadata[] annotations = beans[0].getAnnotations(); + assertEquals(2, annotations.length); + + AnnotationMetadata qualifierAnnotation = annotations[0]; + AnnotationMetadata profileAnnotation = annotations[1]; + + assertEquals("org.springframework.beans.factory.annotation.Qualifier", qualifierAnnotation.getAnnotationType()); + assertFalse(qualifierAnnotation.isMetaAnnotation()); + + Map qualifierAttributes = qualifierAnnotation.getAttributes(); + assertEquals(1, qualifierAttributes.size()); + assertArrayEquals(new String[] {"repoQualifier"}, qualifierAttributes.get("value")); + + assertEquals("org.springframework.context.annotation.Profile", profileAnnotation.getAnnotationType()); + assertFalse(profileAnnotation.isMetaAnnotation()); + + Map profileAttributes = profileAnnotation.getAttributes(); + assertEquals(1, profileAttributes.size()); + assertArrayEquals(new String[] {"prof1", "prof2"}, profileAttributes.get("value")); + } + + + } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java index d155241a8..e3f4886d0 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java @@ -165,7 +165,7 @@ public class DependsOnCompletionProviderTest { assertCompletions("@DependsOn({\"bean1\",<*>\"bean2\"})", 1, "@DependsOn({\"bean1\",\"bean3\",<*>\"bean2\"})"); } - private void assertCompletions(String completionLine, int noOfExcpectedCompletions, String expectedCompletedLine) throws Exception { + private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { String editorContent = """ package org.test; @@ -182,9 +182,9 @@ public class DependsOnCompletionProviderTest { Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); List completions = editor.getCompletions(); - assertEquals(noOfExcpectedCompletions, completions.size()); + assertEquals(noOfExpectedCompletions, completions.size()); - if (noOfExcpectedCompletions > 0) { + if (noOfExpectedCompletions > 0) { editor.apply(completions.get(0)); assertEquals(""" package org.test; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierCompletionProviderTest.java new file mode 100644 index 000000000..8a19c2ebe --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierCompletionProviderTest.java @@ -0,0 +1,222 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class QualifierCompletionProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringMetamodelIndex springIndex; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private IJavaProject project; + private Bean[] indexedBeans; + private String tempJavaDocUri; + private Bean bean1; + private Bean bean2; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI()); + + String projectDir = directory.toURI().toString(); + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + + indexedBeans = springIndex.getBeansOfProject(project.getElementName()); + + tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); + AnnotationMetadata annotationBean1 = new AnnotationMetadata("org.springframework.beans.factory.annotation.Qualifier", false, Map.of("value", new String[] {"quali1"})); + AnnotationMetadata annotationBean2 = new AnnotationMetadata("org.springframework.beans.factory.annotation.Qualifier", false, Map.of("value", new String[] {"quali2"})); + + bean1 = new Bean("bean1", "type1", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean1}); + bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean2}); + + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2}); + } + + @AfterEach + public void restoreIndexState() { + this.springIndex.updateBeans(project.getElementName(), indexedBeans); + } + + @Test + public void testQualifierCompletionWithoutQuotesWithoutPrefix() throws Exception { + assertCompletions("@Qualifier(<*>)", 4, new String[] {"bean1", "bean2", "quali1", "quali2"}, 0, "@Qualifier(\"bean1\"<*>)"); + } + +// @Test +// public void testDependsOnCompletionWithoutQuotesWithPrefix() throws Exception { +// assertCompletions("@DependsOn(be<*>)", 2, "@DependsOn(\"bean1\"<*>)"); +// } +// +// @Test +// public void testDependsOnCompletionWithoutQuotesWithAttributeName() throws Exception { +// assertCompletions("@DependsOn(value=<*>)", 2, "@DependsOn(value=\"bean1\"<*>)"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfQuotesWithoutPrefix() throws Exception { +// assertCompletions("@DependsOn(\"<*>\")", 2, "@DependsOn(\"bean1<*>\")"); +// } +// +// @Test +// public void testDependsOnCompletionWithoutQuotesWithoutPrefixInsideArray() throws Exception { +// assertCompletions("@DependsOn({<*>})", 2, "@DependsOn({\"bean1\"<*>})"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfQuotesWithoutPrefixInsideArray() throws Exception { +// assertCompletions("@DependsOn({\"<*>\"})", 2, "@DependsOn({\"bean1<*>\"})"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfQuotesWithPrefix() throws Exception { +// assertCompletions("@DependsOn(\"be<*>\")", 2, "@DependsOn(\"bean1<*>\")"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfQuotesAndArrayWithPrefix() throws Exception { +// assertCompletions("@DependsOn({\"be<*>\"})", 2, "@DependsOn({\"bean1<*>\"})"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfQuotesWithPrefixButWithoutMatches() throws Exception { +// assertCompletions("@DependsOn(\"XXX<*>\")", 0, null); +// } +// +// @Test +// public void testDependsOnCompletionOutsideOfAnnotation1() throws Exception { +// assertCompletions("@DependsOn(\"XXX\")<*>", 0, null); +// } +// +// @Test +// public void testDependsOnCompletionOutsideOfAnnotation2() throws Exception { +// assertCompletions("@DependsOn<*>(\"XXX\")", 0, null); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfQuotesWithPrefixAndReplacedPostfix() throws Exception { +// assertCompletions("@DependsOn(\"be<*>xxx\")", 2, "@DependsOn(\"bean1<*>xxx\")"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfArrayBehindExistingElement() throws Exception { +// assertCompletions("@DependsOn({\"bean1\",<*>})", 1, "@DependsOn({\"bean1\",\"bean2\"<*>})"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfArrayInFrontOfExistingElement() throws Exception { +// assertCompletions("@DependsOn({<*>\"bean1\"})", 1, "@DependsOn({\"bean2\",<*>\"bean1\"})"); +// } +// +// @Test +// public void testDependsOnCompletionInsideOfArrayBetweenExistingElements() throws Exception { +// Bean bean3 = new Bean("bean3", "type3", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null); +// springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3}); +// +// assertCompletions("@DependsOn({\"bean1\",<*>\"bean2\"})", 1, "@DependsOn({\"bean1\",\"bean3\",<*>\"bean2\"})"); +// } + + private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + assertCompletions(completionLine, noOfExpectedCompletions, null, 0, expectedCompletedLine); + } + + private void assertCompletions(String completionLine, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import org.springframework.beans.factory.annotation.Qualifier; + + @Component + """ + + completionLine + "\n" + + """ + public class TestDependsOnClass { + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + + List completions = editor.getCompletions(); + assertEquals(noOfExcpectedCompletions, completions.size()); + + if (expectedCompletions != null) { + String[] completionItems = completions.stream() + .map(item -> item.getLabel()) + .toArray(size -> new String[size]); + + assertArrayEquals(expectedCompletions, completionItems); + } + + if (noOfExcpectedCompletions > 0) { + editor.apply(completions.get(chosenCompletion)); + assertEquals(""" + package org.test; + + import org.springframework.beans.factory.annotation.Qualifier; + + @Component + """ + expectedCompletedLine + "\n" + + """ + public class TestDependsOnClass { + } + """, editor.getText()); + } + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierDefinitionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierDefinitionProviderTest.java new file mode 100644 index 000000000..72826a51e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierDefinitionProviderTest.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans.test; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class QualifierDefinitionProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringMetamodelIndex springIndex; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private IJavaProject project; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI()); + + String projectDir = directory.toURI().toString(); + project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + public void testQualifierRefersToBeanDefinitionLink() throws Exception { + String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.beans.factory.annotation.Qualifier; + + @Component + @Qualifier("bean1") + public class TestDependsOnClass { + }""", tempJavaDocUri); + + String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString(); + + Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); + assertEquals(1, beans.length); + + LocationLink expectedLocation = new LocationLink(expectedDefinitionUri, + beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), + null); + + editor.assertLinkTargets("bean1", List.of(expectedLocation)); + } + +// @Test +// public void testMultipleDependsOnBeanDefinitionLink() throws Exception { +// String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); +// +// Editor editor = harness.newEditor(LanguageId.JAVA, """ +// package org.test; +// +// import org.springframework.context.annotation.DependsOn; +// +// @Component +// @DependsOn({"bean1", "bean2"}) +// public class TestDependsOnClass { +// }""", tempJavaDocUri); +// +// String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString(); +// +// Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); +// assertEquals(1, beans.length); +// +// LocationLink expectedLocation = new LocationLink(expectedDefinitionUri, +// beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), +// null); +// +// editor.assertLinkTargets("bean1", List.of(expectedLocation)); +// } +// +// @Test +// public void testDependsOnWithMultipleBeanDefinitionLinks() throws Exception { +// String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); +// +// Editor editor = harness.newEditor(LanguageId.JAVA, """ +// package org.test; +// +// import org.springframework.context.annotation.DependsOn; +// +// @Component +// @DependsOn("bean1") +// public class TestDependsOnClass { +// }""", tempJavaDocUri); +// +// String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString(); +// +// List beansOfDoc = new ArrayList<>(List.of(springIndex.getBeansOfDocument(expectedDefinitionUri))); +// beansOfDoc.add(new Bean("bean1", "type", new Location(expectedDefinitionUri, new Range(new Position(20, 1), new Position(20, 10))), null, null, null)); +// springIndex.updateBeans(project.getElementName(), expectedDefinitionUri, beansOfDoc.toArray(new Bean[0])); +// +// Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); +// assertEquals(2, beans.length); +// +// LocationLink expectedLocation1 = new LocationLink(expectedDefinitionUri, +// beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), +// null); +// +// LocationLink expectedLocation2 = new LocationLink(expectedDefinitionUri, +// beans[1].getLocation().getRange(), beans[1].getLocation().getRange(), +// null); +// +// editor.assertLinkTargets("bean1", List.of(expectedLocation1, expectedLocation2)); +// } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/MainClass.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/MainClass.java index cc4fd96b4..ef6b4e1a1 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/MainClass.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/MainClass.java @@ -3,6 +3,8 @@ package org.test; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.beans.factory.annotation.Qualifier; @SpringBootApplication public class MainClass { @@ -21,4 +23,11 @@ public class MainClass { return new BeanClass2(); } + @Bean + @Qualifier("qualifier1") + @Profile({"testprofile","testprofile2"}) + BeanClass2 bean3() { + return new BeanClass2(); + } + } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/AutowiredInjectionService.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/AutowiredInjectionService.java index e2b91394e..c5dc8fb11 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/AutowiredInjectionService.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/AutowiredInjectionService.java @@ -1,6 +1,7 @@ package org.test.injections; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.test.BeanClass1; import org.test.BeanClass2; @@ -8,8 +9,12 @@ import org.test.BeanClass2; @Service public class AutowiredInjectionService { - @Autowired private BeanClass1 bean1; - @Autowired private BeanClass2 bean2; + @Autowired + private BeanClass1 bean1; + + @Autowired + @Qualifier("qual1") + private BeanClass2 bean2; public BeanClass1 getBean1() { return bean1; diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/ConfigurationWithInjectionsAndAnnotations.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/ConfigurationWithInjectionsAndAnnotations.java new file mode 100644 index 000000000..0692b31d9 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/injections/ConfigurationWithInjectionsAndAnnotations.java @@ -0,0 +1,25 @@ +package org.test.injections; + +import org.test.MainClass; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.beans.factory.annotation.Qualifier; +import org.test.BeanClass1; +import org.test.BeanClass2; +import org.test.ManuallyCreatedBeanWithConstructor; + +@Configuration +@Qualifier("qualifier") +@ImportRuntimeHints(MainClass.class) +public class ConfigurationWithInjectionsAndAnnotations { + + @Bean + @DependsOn({"bean1", "bean2"}) + ManuallyCreatedBeanWithConstructor beanWithAnnotationsOnInjectionPoints(@Qualifier("q1") BeanClass1 bean1, @Qualifier("q2") BeanClass2 bean2) { + return new ManuallyCreatedBeanWithConstructor(bean1, bean2); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/springdata/CustomerRepository.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/springdata/CustomerRepository.java index 78690b7a6..86eaa3b61 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/springdata/CustomerRepository.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/springdata/CustomerRepository.java @@ -2,9 +2,14 @@ package org.test.springdata; import java.util.List; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Profile; import org.springframework.data.repository.CrudRepository; +@Qualifier("repoQualifier") +@Profile({"prof1", "prof2"}) public interface CustomerRepository extends CrudRepository { List findByLastName(String lastName); + }