Skip to content

Commit 8552a52

Browse files
karthiksmsmartinlippert
authored andcommitted
Add support for @ConditionalOnResource annotation attributes
1 parent 23203a1 commit 8552a52

File tree

16 files changed

+1086
-0
lines changed

16 files changed

+1086
-0
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
4242
import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor;
4343
import org.springframework.ide.vscode.boot.java.contextconfiguration.ContextConfigurationProcessor;
44+
import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceProcessor;
4445
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
4546
import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider;
4647
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
@@ -118,6 +119,7 @@ BootJavaCompletionEngine javaCompletionEngine(
118119

119120
providers.put(Annotations.VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties));
120121
providers.put(Annotations.CONTEXT_CONFIGURATION, new ContextConfigurationProcessor(javaProjectFinder));
122+
providers.put(Annotations.CONDITIONAL_ON_RESOURCE, new ConditionalOnResourceProcessor(javaProjectFinder));
121123
providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor());
122124

123125
providers.put(Annotations.SCOPE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ScopeCompletionProcessor())));

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

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
7373
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
7474
import org.springframework.ide.vscode.boot.java.value.ValueDefinitionProvider;
75+
import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceDefinitionProvider;
7576
import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService;
7677
import org.springframework.ide.vscode.boot.jdt.ls.JdtLsProjectCache;
7778
import org.springframework.ide.vscode.boot.metadata.AdHocSpringPropertyIndexProvider;
@@ -400,6 +401,7 @@ BootJavaCodeActionProvider getBootJavaCodeActionProvider(JavaProjectFinder proje
400401
JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, CompilationUnitCache cuCache, JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex, JdtDataQuerySemanticTokensProvider qurySemanticTokens) {
401402
return new JavaDefinitionHandler(cuCache, projectFinder, List.of(
402403
new ValueDefinitionProvider(),
404+
new ConditionalOnResourceDefinitionProvider(),
403405
new DependsOnDefinitionProvider(springIndex),
404406
new ResourceDefinitionProvider(springIndex),
405407
new QualifierDefinitionProvider(springIndex),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Broadcom, Inc.
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, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.conditionalonresource;
12+
13+
import java.nio.file.Paths;
14+
import java.util.ArrayList;
15+
import java.util.Collections;
16+
import java.util.Comparator;
17+
import java.util.HashMap;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.Optional;
21+
22+
import org.eclipse.jdt.core.dom.ASTNode;
23+
import org.eclipse.jdt.core.dom.Annotation;
24+
import org.eclipse.jdt.core.dom.CompilationUnit;
25+
import org.eclipse.jdt.core.dom.Expression;
26+
import org.eclipse.jdt.core.dom.IAnnotationBinding;
27+
import org.eclipse.jdt.core.dom.MemberValuePair;
28+
import org.eclipse.jdt.core.dom.NormalAnnotation;
29+
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
30+
import org.eclipse.jdt.core.dom.StringLiteral;
31+
import org.eclipse.lsp4j.Location;
32+
import org.eclipse.lsp4j.LocationLink;
33+
import org.eclipse.lsp4j.Position;
34+
import org.eclipse.lsp4j.Range;
35+
import org.eclipse.lsp4j.TextDocumentIdentifier;
36+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
37+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
40+
import org.springframework.ide.vscode.boot.java.Annotations;
41+
import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider;
42+
import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents;
43+
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
44+
import org.springframework.ide.vscode.commons.java.IJavaProject;
45+
import org.yaml.snakeyaml.nodes.Node;
46+
47+
import com.google.common.collect.ImmutableList;
48+
import com.google.common.collect.ImmutableList.Builder;
49+
50+
public class ConditionalOnResourceDefinitionProvider implements IJavaDefinitionProvider {
51+
52+
private static final Logger log = LoggerFactory.getLogger(ConditionalOnResourceDefinitionProvider.class);
53+
54+
@Override
55+
public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject project,
56+
TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) {
57+
58+
if (n instanceof StringLiteral) {
59+
StringLiteral valueNode = (StringLiteral) n;
60+
61+
String literalValue = valueNode.getLiteralValue();
62+
if (literalValue != null) {
63+
if (literalValue.startsWith("classpath")) {
64+
return getDefinitionForClasspathResource(project, cu, valueNode, literalValue);
65+
}
66+
}
67+
}
68+
return Collections.emptyList();
69+
}
70+
71+
72+
private List<LocationLink> getDefinitionForClasspathResource(IJavaProject project, CompilationUnit cu, StringLiteral valueNode, String literalValue) {
73+
literalValue = literalValue.substring("classpath:".length());
74+
75+
String[] resources = findResources(project, literalValue);
76+
77+
List<LocationLink> result = new ArrayList<>();
78+
79+
for (String resource : resources) {
80+
String uri = "file://" + resource;
81+
82+
Position startPosition = new Position(cu.getLineNumber(valueNode.getStartPosition()) - 1,
83+
cu.getColumnNumber(valueNode.getStartPosition()));
84+
Position endPosition = new Position(
85+
cu.getLineNumber(valueNode.getStartPosition() + valueNode.getLength()) - 1,
86+
cu.getColumnNumber(valueNode.getStartPosition() + valueNode.getLength()));
87+
Range nodeRange = new Range(startPosition, endPosition);
88+
89+
LocationLink locationLink = new LocationLink(uri,
90+
new Range(new Position(0, 0), new Position(0, 0)), new Range(new Position(0, 0), new Position(0, 0)),
91+
nodeRange);
92+
93+
result.add(locationLink);
94+
}
95+
96+
return result;
97+
}
98+
99+
private String[] findResources(IJavaProject project, String resource) {
100+
String[] resources = IClasspathUtil.getClasspathResourcesFullPaths(project.getClasspath())
101+
.filter(path -> path.toString().endsWith(resource))
102+
.map(path -> path.toString())
103+
.toArray(String[]::new);
104+
105+
return resources;
106+
}
107+
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Broadcom, Inc.
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, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.conditionalonresource;
12+
13+
import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens;
14+
15+
import java.nio.file.Paths;
16+
import java.util.*;
17+
import java.util.stream.Collectors;
18+
19+
import org.eclipse.jdt.core.dom.ASTNode;
20+
import org.eclipse.jdt.core.dom.Annotation;
21+
import org.eclipse.jdt.core.dom.ITypeBinding;
22+
import org.eclipse.jdt.core.dom.MemberValuePair;
23+
import org.eclipse.jdt.core.dom.QualifiedName;
24+
import org.eclipse.jdt.core.dom.SimpleName;
25+
import org.eclipse.jdt.core.dom.StringLiteral;
26+
import org.eclipse.lsp4j.TextDocumentIdentifier;
27+
import org.openrewrite.yaml.internal.grammar.JsonPathParser;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProposal;
31+
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
32+
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
33+
import org.springframework.ide.vscode.boot.metadata.PropertyInfo;
34+
import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider;
35+
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
36+
import org.springframework.ide.vscode.commons.java.IJavaProject;
37+
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
38+
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
39+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
40+
import org.springframework.ide.vscode.commons.util.BadLocationException;
41+
import org.springframework.ide.vscode.commons.util.FuzzyMap;
42+
import org.springframework.ide.vscode.commons.util.FuzzyMap.Match;
43+
import org.springframework.ide.vscode.commons.util.text.IDocument;
44+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
45+
46+
/**
47+
* @author Karthik Sankaranarayanan
48+
*/
49+
public class ConditionalOnResourceProcessor implements CompletionProvider {
50+
51+
private static final Logger log = LoggerFactory.getLogger(ConditionalOnResourceProcessor.class);
52+
53+
private final JavaProjectFinder projectFinder;
54+
55+
public ConditionalOnResourceProcessor(JavaProjectFinder projectFinder) {
56+
this.projectFinder = projectFinder;
57+
}
58+
59+
@Override
60+
public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type,
61+
int offset, TextDocument doc, Collection<ICompletionProposal> completions) {
62+
63+
try {
64+
Optional<IJavaProject> optionalProject = this.projectFinder.find(doc.getId());
65+
if (optionalProject.isEmpty()) {
66+
return;
67+
}
68+
69+
IJavaProject project = optionalProject.get();
70+
71+
// case: @ConditionalOnResource(resources=<*>)
72+
if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair
73+
&& ("resources".equals(((MemberValuePair)node.getParent()).getName().toString()))) {
74+
computeProposalsForSimpleName(project, node, completions, offset, doc);
75+
}
76+
// case: @ConditionalOnResource(resources=<*>)
77+
else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof MemberValuePair
78+
&& ("resources".equals(((MemberValuePair)node.getParent().getParent()).getName().toString()))) {
79+
computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc);
80+
}
81+
// case:@ConditionalOnResource(resources="prefix<*>")
82+
else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair
83+
&& ("resources".equals(((MemberValuePair)node.getParent()).getName().toString()))) {
84+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
85+
computeProposalsForStringLiteral(project, (StringLiteral) node, completions, offset, doc);
86+
}
87+
}
88+
}
89+
catch (Exception e) {
90+
log.error("problem while looking for ConditionalOnResource annotation proposals", e);
91+
}
92+
}
93+
94+
private void addClasspathResourceProposals(IJavaProject project, TextDocument doc, int startOffset, int endOffset, String prefix, boolean includeQuotes, Collection<ICompletionProposal> completions) {
95+
String[] resources = findResources(project, prefix);
96+
97+
double score = resources.length + 1000;
98+
for (String resource : resources) {
99+
100+
DocumentEdits edits = new DocumentEdits(doc, false);
101+
102+
if (includeQuotes) {
103+
edits.replace(startOffset, endOffset, "\"classpath:" + resource + "\"");
104+
}
105+
else {
106+
edits.replace(startOffset, endOffset, "classpath:" + resource);
107+
}
108+
109+
String label = "classpath:" + resource;
110+
111+
ICompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, label, label, null, score--);
112+
completions.add(proposal);
113+
}
114+
115+
}
116+
117+
private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) {
118+
int startOffset = node.getStartPosition();
119+
int endOffset = node.getStartPosition() + node.getLength();
120+
121+
String unfilteredPrefix = node.toString().substring(0, offset - node.getStartPosition());
122+
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, true, completions);
123+
}
124+
125+
private void computeProposalsForStringLiteral(IJavaProject project, StringLiteral node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) throws BadLocationException {
126+
String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, offset - (node.getStartPosition() + 1)), offset - (node.getStartPosition() + 1));
127+
128+
int startOffset = offset - prefix.length();
129+
int endOffset = offset;
130+
131+
String unfilteredPrefix = node.getLiteralValue().substring(0, offset - (node.getStartPosition() + 1));
132+
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, false, completions);
133+
}
134+
135+
public String identifyPropertyPrefix(String nodeContent, int offset) {
136+
String result = nodeContent.substring(0, offset);
137+
138+
int i = offset - 1;
139+
while (i >= 0) {
140+
char c = nodeContent.charAt(i);
141+
if (c == '}' || c == '{' || c == '$' || c == '#') {
142+
result = result.substring(i + 1, offset);
143+
break;
144+
}
145+
i--;
146+
}
147+
148+
return result;
149+
}
150+
151+
private String[] findResources(IJavaProject project, String prefix) {
152+
String[] resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream()
153+
.distinct()
154+
.sorted(new Comparator<String>() {
155+
@Override
156+
public int compare(String o1, String o2) {
157+
return Paths.get(o1).compareTo(Paths.get(o2));
158+
}
159+
})
160+
.map(r -> r.replaceAll("\\\\", "/"))
161+
.filter(r -> ("classpath:" + r).contains(prefix))
162+
.toArray(String[]::new);
163+
164+
return resources;
165+
}
166+
167+
}

0 commit comments

Comments
 (0)