Skip to content

Commit 5bc3832

Browse files
committed
GH-1294: add content-assist for profile annotation values
1 parent 3eab0c7 commit 5bc3832

File tree

3 files changed

+297
-1
lines changed

3 files changed

+297
-1
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProcessor;
2727
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
2828
import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor;
29+
import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider;
2930
import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider;
3031
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
3132
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
@@ -118,7 +119,7 @@ BootJavaCompletionEngine javaCompletionEngine(
118119
providers.put(Annotations.SCOPE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ScopeCompletionProcessor())));
119120
providers.put(Annotations.DEPENDS_ON, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new DependsOnCompletionProcessor(springIndex))));
120121
providers.put(Annotations.QUALIFIER, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new QualifierCompletionProvider(springIndex))));
121-
122+
providers.put(Annotations.PROFILE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ProfileCompletionProvider(springIndex))));
122123

123124
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
124125
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.beans;
12+
13+
import java.util.Arrays;
14+
import java.util.List;
15+
import java.util.stream.Stream;
16+
17+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
18+
import org.springframework.ide.vscode.boot.java.Annotations;
19+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider;
20+
import org.springframework.ide.vscode.commons.java.IJavaProject;
21+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
22+
23+
/**
24+
* @author Martin Lippert
25+
*/
26+
public class ProfileCompletionProvider implements AnnotationAttributeCompletionProvider {
27+
28+
private final SpringMetamodelIndex springIndex;
29+
30+
public ProfileCompletionProvider(SpringMetamodelIndex springIndex) {
31+
this.springIndex = springIndex;
32+
}
33+
34+
@Override
35+
public List<String> getCompletionCandidates(IJavaProject project) {
36+
37+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
38+
39+
return findAllProfiles(beans)
40+
.distinct()
41+
.toList();
42+
}
43+
44+
private Stream<String> findAllProfiles(Bean[] beans) {
45+
46+
Stream<String> profilesFromBeans = Arrays.stream(beans)
47+
// annotations from beans themselves
48+
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
49+
.filter(annotation -> Annotations.PROFILE.equals(annotation.getAnnotationType()))
50+
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value"))
51+
.flatMap(annotation -> Arrays.stream(annotation.getAttributes().get("value")));
52+
53+
Stream<String> profilesFromInjectionPoints = Arrays.stream(beans)
54+
// annotations from beans themselves
55+
.filter(bean -> bean.getInjectionPoints() != null)
56+
.flatMap(bean -> Arrays.stream(bean.getInjectionPoints()))
57+
.filter(injectionPoint -> injectionPoint.getAnnotations() != null)
58+
.flatMap(injectionPoint -> Arrays.stream(injectionPoint.getAnnotations()))
59+
.filter(annotation -> Annotations.PROFILE.equals(annotation.getAnnotationType()))
60+
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value"))
61+
.flatMap(annotation -> Arrays.stream(annotation.getAttributes().get("value")));
62+
63+
return Stream.concat(profilesFromBeans, profilesFromInjectionPoints);
64+
}
65+
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.beans.test;
12+
13+
import static org.junit.Assert.assertArrayEquals;
14+
import static org.junit.Assert.assertEquals;
15+
16+
import java.io.File;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.TimeUnit;
21+
22+
import org.eclipse.lsp4j.CompletionItem;
23+
import org.eclipse.lsp4j.Location;
24+
import org.eclipse.lsp4j.Position;
25+
import org.eclipse.lsp4j.Range;
26+
import org.eclipse.lsp4j.TextDocumentIdentifier;
27+
import org.junit.jupiter.api.AfterEach;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.ExtendWith;
31+
import org.springframework.beans.factory.annotation.Autowired;
32+
import org.springframework.context.annotation.Import;
33+
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
34+
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
35+
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
36+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
37+
import org.springframework.ide.vscode.commons.java.IJavaProject;
38+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
39+
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
40+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
41+
import org.springframework.ide.vscode.commons.util.text.LanguageId;
42+
import org.springframework.ide.vscode.languageserver.testharness.Editor;
43+
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
44+
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
45+
import org.springframework.test.context.junit.jupiter.SpringExtension;
46+
47+
/**
48+
* @author Martin Lippert
49+
*/
50+
@ExtendWith(SpringExtension.class)
51+
@BootLanguageServerTest
52+
@Import(SymbolProviderTestConf.class)
53+
public class ProfileCompletionProviderTest {
54+
55+
@Autowired private BootLanguageServerHarness harness;
56+
@Autowired private JavaProjectFinder projectFinder;
57+
@Autowired private SpringMetamodelIndex springIndex;
58+
@Autowired private SpringSymbolIndex indexer;
59+
60+
private File directory;
61+
private IJavaProject project;
62+
private Bean[] indexedBeans;
63+
private String tempJavaDocUri;
64+
private Bean bean1;
65+
private Bean bean2;
66+
private String[] allProposals;
67+
68+
@BeforeEach
69+
public void setup() throws Exception {
70+
harness.intialize(null);
71+
72+
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI());
73+
74+
String projectDir = directory.toURI().toString();
75+
project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
76+
77+
CompletableFuture<Void> initProject = indexer.waitOperation();
78+
initProject.get(5, TimeUnit.SECONDS);
79+
80+
indexedBeans = springIndex.getBeansOfProject(project.getElementName());
81+
82+
tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
83+
AnnotationMetadata annotationBean1 = new AnnotationMetadata("org.springframework.context.annotation.Profile", false, Map.of("value", new String[] {"prof1", "prof2"}));
84+
AnnotationMetadata annotationBean2 = new AnnotationMetadata("org.springframework.context.annotation.Profile", false, Map.of("value", new String[] {"prof3"}));
85+
86+
bean1 = new Bean("bean1", "type1", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean1});
87+
bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean2});
88+
89+
springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2});
90+
91+
allProposals = new String[] {"prof1", "prof2", "prof3"};
92+
}
93+
94+
@AfterEach
95+
public void restoreIndexState() {
96+
this.springIndex.updateBeans(project.getElementName(), indexedBeans);
97+
}
98+
99+
@Test
100+
public void testProfileCompletionWithoutQuotesWithoutPrefix() throws Exception {
101+
assertCompletions("@Profile(<*>)", allProposals, 0, "@Profile(\"prof1\"<*>)");
102+
}
103+
104+
@Test
105+
public void testProfileCompletionWithoutQuotesWithPrefix() throws Exception {
106+
assertCompletions("@Profile(prof1<*>)", new String[] {"prof1"}, 0, "@Profile(\"prof1\"<*>)");
107+
}
108+
109+
@Test
110+
public void testProfileCompletionWithoutQuotesWithAttributeName() throws Exception {
111+
assertCompletions("@Profile(value=<*>)", allProposals, 0, "@Profile(value=\"prof1\"<*>)");
112+
}
113+
114+
@Test
115+
public void testProfileCompletionInsideOfQuotesWithoutPrefix() throws Exception {
116+
assertCompletions("@Profile(\"<*>\")", allProposals, 0, "@Profile(\"prof1<*>\")");
117+
}
118+
119+
@Test
120+
public void testProfileCompletionInsideOfQuotesWithPrefix() throws Exception {
121+
assertCompletions("@Profile(\"prof1<*>\")", new String[] {"prof1"}, 0, "@Profile(\"prof1<*>\")");
122+
}
123+
124+
@Test
125+
public void testProfileCompletionInsideOfQuotesAndArrayWithPrefix() throws Exception {
126+
assertCompletions("@Profile({\"pr<*>\"})", allProposals, 0, "@Profile({\"prof1<*>\"})");
127+
}
128+
129+
@Test
130+
public void testProfileCompletionInsideOfQuotesWithoutPrefixInsideArray() throws Exception {
131+
assertCompletions("@Profile({\"<*>\"})", allProposals, 0, "@Profile({\"prof1<*>\"})");
132+
}
133+
134+
@Test
135+
public void testProfileCompletionWithoutQuotesWithoutPrefixInsideArray() throws Exception {
136+
assertCompletions("@Profile({<*>})", allProposals, 0, "@Profile({\"prof1\"<*>})");
137+
}
138+
139+
@Test
140+
public void testProfileCompletionInsideOfArrayBehindExistingElement() throws Exception {
141+
assertCompletions("@Profile({\"prof1\",<*>})", new String[] {"prof2", "prof3"}, 0, "@Profile({\"prof1\",\"prof2\"<*>})");
142+
}
143+
144+
@Test
145+
public void testProfileCompletionInsideOfArrayInFrontOfExistingElement() throws Exception {
146+
assertCompletions("@Profile({<*>\"prof1\"})", new String[] {"prof2", "prof3"}, 0, "@Profile({\"prof2\",<*>\"prof1\"})");
147+
}
148+
149+
@Test
150+
public void testProfileCompletionInsideOfArrayBetweenExistingElements() throws Exception {
151+
assertCompletions("@Profile({\"prof1\",<*>\"prof2\"})", new String[] {"prof3"}, 0, "@Profile({\"prof1\",\"prof3\",<*>\"prof2\"})");
152+
}
153+
154+
@Test
155+
public void testProfileCompletionInsideOfQuotesWithPrefixButWithoutMatches() throws Exception {
156+
assertCompletions("@Profile(\"XXX<*>\")", 0, null);
157+
}
158+
159+
@Test
160+
public void testProfileCompletionOutsideOfAnnotation1() throws Exception {
161+
assertCompletions("@Profile(\"XXX\")<*>", 0, null);
162+
}
163+
164+
@Test
165+
public void testProfileCompletionOutsideOfAnnotation2() throws Exception {
166+
assertCompletions("@Profile<*>(\"XXX\")", 0, null);
167+
}
168+
169+
@Test
170+
public void testProfileCompletionInsideOfQuotesWithPrefixAndReplacedPostfix() throws Exception {
171+
assertCompletions("@Profile(\"pro<*>xxx\")", allProposals, 0, "@Profile(\"prof1<*>\")");
172+
}
173+
174+
private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception {
175+
assertCompletions(completionLine, noOfExpectedCompletions, null, 0, expectedCompletedLine);
176+
}
177+
178+
private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception {
179+
assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedCompletedLine);
180+
}
181+
182+
183+
private void assertCompletions(String completionLine, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception {
184+
String editorContent = """
185+
package org.test;
186+
187+
import org.springframework.stereotype.Component;
188+
import org.springframework.context.annotation.Profile;
189+
190+
@Component
191+
""" +
192+
completionLine + "\n" +
193+
"""
194+
public class TestDependsOnClass {
195+
}
196+
""";
197+
198+
Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
199+
200+
List<CompletionItem> completions = editor.getCompletions();
201+
assertEquals(noOfExcpectedCompletions, completions.size());
202+
203+
if (expectedCompletions != null) {
204+
String[] completionItems = completions.stream()
205+
.map(item -> item.getLabel())
206+
.toArray(size -> new String[size]);
207+
208+
assertArrayEquals(expectedCompletions, completionItems);
209+
}
210+
211+
if (noOfExcpectedCompletions > 0) {
212+
editor.apply(completions.get(chosenCompletion));
213+
assertEquals("""
214+
package org.test;
215+
216+
import org.springframework.stereotype.Component;
217+
import org.springframework.context.annotation.Profile;
218+
219+
@Component
220+
""" + expectedCompletedLine + "\n" +
221+
"""
222+
public class TestDependsOnClass {
223+
}
224+
""", editor.getText());
225+
}
226+
}
227+
228+
229+
}

0 commit comments

Comments
 (0)