Skip to content

Commit 980f392

Browse files
committed
GH-1494: add special index nodes for configuration property details
1 parent 0f759b1 commit 980f392

File tree

14 files changed

+370
-10
lines changed

14 files changed

+370
-10
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup;
1818
import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider;
1919
import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider;
20+
import org.springframework.ide.vscode.boot.java.beans.ConfigurationPropertiesSymbolProvider;
2021
import org.springframework.ide.vscode.boot.java.beans.FeignClientSymbolProvider;
2122
import org.springframework.ide.vscode.boot.java.data.DataRepositorySymbolProvider;
2223
import org.springframework.ide.vscode.boot.java.events.EventListenerSymbolProvider;
@@ -34,10 +35,12 @@ AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache)
3435
RequestMappingSymbolProvider requestMappingSymbolProvider = new RequestMappingSymbolProvider();
3536
BeansSymbolProvider beansSymbolProvider = new BeansSymbolProvider();
3637
ComponentSymbolProvider componentSymbolProvider = new ComponentSymbolProvider();
37-
RestrictedDefaultSymbolProvider restrictedDefaultSymbolProvider = new RestrictedDefaultSymbolProvider();
38+
ConfigurationPropertiesSymbolProvider configPropsSymbolProvider = new ConfigurationPropertiesSymbolProvider();
3839
DataRepositorySymbolProvider dataRepositorySymbolProvider = new DataRepositorySymbolProvider();
3940
EventListenerSymbolProvider eventListenerSymbolProvider = new EventListenerSymbolProvider();
4041

42+
RestrictedDefaultSymbolProvider restrictedDefaultSymbolProvider = new RestrictedDefaultSymbolProvider();
43+
4144
providers.put(Annotations.SPRING_REQUEST_MAPPING, requestMappingSymbolProvider);
4245
providers.put(Annotations.SPRING_GET_MAPPING, requestMappingSymbolProvider);
4346
providers.put(Annotations.SPRING_POST_MAPPING, requestMappingSymbolProvider);
@@ -49,6 +52,7 @@ AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache)
4952
providers.put(Annotations.COMPONENT, componentSymbolProvider);
5053
providers.put(Annotations.NAMED_JAKARTA, componentSymbolProvider);
5154
providers.put(Annotations.NAMED_JAVAX, componentSymbolProvider);
55+
providers.put(Annotations.CONFIGURATION_PROPERTIES, configPropsSymbolProvider);
5256

5357
providers.put(Annotations.PROFILE, restrictedDefaultSymbolProvider);
5458

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public class Annotations {
2828
public static final String COMPONENT = "org.springframework.stereotype.Component";
2929
public static final String CONFIGURATION = "org.springframework.context.annotation.Configuration";
3030
public static final String CONTROLLER = "org.springframework.stereotype.Controller";
31+
32+
public static final String CONFIGURATION_PROPERTIES = "org.springframework.boot.context.properties.ConfigurationProperties";
3133

3234
public static final String REPOSITORY = "org.springframework.stereotype.Repository";
3335
public static final String REPOSITORY_DEFINITION = "org.springframework.data.repository.RepositoryDefinition";

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ public static String getBeanNameFromComponentAnnotation(Annotation annotation, T
3939
return ASTUtils.getExpressionValueAsString(attribute.get(), (a) -> {});
4040
}
4141
else {
42-
String beanName = type.getName().toString();
43-
return BeanUtils.getBeanNameFromType(beanName);
42+
String beanType = type.getName().toString();
43+
return BeanUtils.getBeanNameFromType(beanType);
4444
}
4545
}
4646

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,20 @@ protected void createSymbol(Annotation node, ITypeBinding annotationType, Collec
135135
indexEventListeners(beanDefinition, type, annotationType, metaAnnotations, context, doc);
136136
indexEventListenerInterfaceImplementation(beanDefinition, type, context, doc);
137137
indexRequestMappings(beanDefinition, type, annotationType, metaAnnotations, context, doc);
138+
indexConfigurationProperties(beanDefinition, type, context, doc);
138139

139140
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
140141
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
141142
}
142143

144+
private void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
145+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
146+
147+
if (annotationHierarchies.isAnnotatedWith(type.resolveBinding(), Annotations.CONFIGURATION_PROPERTIES)) {
148+
ConfigurationPropertiesSymbolProvider.indexConfigurationProperties(beanDefinition, type, context, doc);
149+
}
150+
}
151+
143152
private void indexBeanMethods(Bean bean, TypeDeclaration type, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
144153
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
145154
if (bean.isConfiguration()) {
@@ -323,7 +332,7 @@ private MethodDeclaration findHandleEventMethod(TypeDeclaration type) {
323332
return null;
324333
}
325334

326-
protected String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
335+
public static String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
327336
StringBuilder symbolLabel = new StringBuilder();
328337
symbolLabel.append("@");
329338
symbolLabel.append(searchPrefix);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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 org.eclipse.lsp4j.DocumentSymbol;
14+
import org.eclipse.lsp4j.Range;
15+
import org.eclipse.lsp4j.SymbolKind;
16+
import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement;
17+
import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement;
18+
19+
/**
20+
* @author Martin Lippert
21+
*/
22+
public class ConfigPropertyIndexElement extends AbstractSpringIndexElement implements SymbolElement {
23+
24+
private final String name;
25+
private final String type;
26+
private final Range range;
27+
28+
public ConfigPropertyIndexElement(String name, String type, Range range) {
29+
this.name = name;
30+
this.type = type;
31+
this.range = range;
32+
}
33+
34+
public String getName() {
35+
return name;
36+
}
37+
38+
public String getType() {
39+
return type;
40+
}
41+
42+
public Range getRange() {
43+
return range;
44+
}
45+
46+
@Override
47+
public DocumentSymbol getDocumentSymbol() {
48+
DocumentSymbol symbol = new DocumentSymbol();
49+
50+
symbol.setName(name + " (" + getShortTypeName() + ")");
51+
symbol.setKind(SymbolKind.Property);
52+
symbol.setRange(range);
53+
symbol.setSelectionRange(range);
54+
55+
return symbol;
56+
}
57+
58+
private String getShortTypeName() {
59+
if (type != null) {
60+
int i = type.lastIndexOf(".");
61+
if (i >= 0) {
62+
return type.substring(i + 1);
63+
}
64+
else {
65+
return type;
66+
}
67+
}
68+
69+
return "";
70+
}
71+
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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.Collection;
15+
import java.util.HashSet;
16+
import java.util.List;
17+
import java.util.Set;
18+
import java.util.stream.Collectors;
19+
import java.util.stream.Stream;
20+
21+
import org.eclipse.jdt.core.dom.Annotation;
22+
import org.eclipse.jdt.core.dom.FieldDeclaration;
23+
import org.eclipse.jdt.core.dom.ITypeBinding;
24+
import org.eclipse.jdt.core.dom.SimpleName;
25+
import org.eclipse.jdt.core.dom.Type;
26+
import org.eclipse.jdt.core.dom.TypeDeclaration;
27+
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
28+
import org.eclipse.lsp4j.Location;
29+
import org.eclipse.lsp4j.Range;
30+
import org.eclipse.lsp4j.SymbolKind;
31+
import org.eclipse.lsp4j.WorkspaceSymbol;
32+
import org.eclipse.lsp4j.jsonrpc.messages.Either;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
import org.springframework.ide.vscode.boot.java.Annotations;
36+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
37+
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
38+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
39+
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
40+
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
41+
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
42+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
43+
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
44+
import org.springframework.ide.vscode.commons.util.BadLocationException;
45+
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
46+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
47+
48+
/**
49+
* @author Martin Lippert
50+
*/
51+
public class ConfigurationPropertiesSymbolProvider implements SymbolProvider {
52+
53+
private static final Logger log = LoggerFactory.getLogger(ConfigurationPropertiesSymbolProvider.class);
54+
55+
@Override
56+
public void addSymbols(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
57+
try {
58+
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) {
59+
createSymbol(node, annotationType, metaAnnotations, context, doc);
60+
}
61+
}
62+
catch (BadLocationException e) {
63+
log.error("", e);
64+
}
65+
}
66+
67+
protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
68+
String annotationTypeName = annotationType.getName();
69+
70+
Collection<String> metaAnnotationNames = metaAnnotations.stream()
71+
.map(ITypeBinding::getName)
72+
.collect(Collectors.toList());
73+
74+
TypeDeclaration type = (TypeDeclaration) node.getParent();
75+
ITypeBinding typeBinding = type.resolveBinding();
76+
77+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
78+
boolean isComponentAnnotated = annotationHierarchies.isAnnotatedWith(typeBinding, Annotations.COMPONENT);
79+
80+
if (!isComponentAnnotated) {
81+
String beanName = BeanUtils.getBeanNameFromType(type.getName().getFullyQualifiedName());
82+
ITypeBinding beanType = type.resolveBinding();
83+
84+
Location location = new Location(doc.getUri(), doc.toRange(type.getStartPosition(), type.getLength()));
85+
86+
WorkspaceSymbol symbol = new WorkspaceSymbol(
87+
ComponentSymbolProvider.beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface,
88+
Either.forLeft(location));
89+
90+
boolean isConfiguration = false; // otherwise, the ComponentSymbolProvider takes care of the bean definiton for this type
91+
92+
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc);
93+
94+
Set<String> supertypes = new HashSet<>();
95+
ASTUtils.findSupertypes(beanType, supertypes);
96+
97+
Collection<Annotation> annotationsOnType = ASTUtils.getAnnotations(type);
98+
99+
AnnotationMetadata[] annotations = Stream.concat(
100+
Arrays.stream(ASTUtils.getAnnotationsMetadata(annotationsOnType, doc))
101+
,
102+
metaAnnotations.stream()
103+
.map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null, null)))
104+
.toArray(AnnotationMetadata[]::new);
105+
106+
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName());
107+
108+
indexConfigurationProperties(beanDefinition, type, context, doc);
109+
110+
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
111+
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
112+
}
113+
}
114+
115+
public static void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
116+
117+
FieldDeclaration[] fields = type.getFields();
118+
if (fields != null) {
119+
for (FieldDeclaration field : fields) {
120+
try {
121+
Type fieldType = field.getType();
122+
if (fieldType != null) {
123+
124+
@SuppressWarnings("unchecked")
125+
List<VariableDeclarationFragment> fragments = field.fragments();
126+
127+
for (VariableDeclarationFragment fragment : fragments) {
128+
SimpleName name = fragment.getName();
129+
130+
if (name != null) {
131+
132+
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, field);
133+
Range range = doc.toRange(nodeRegion);
134+
ConfigPropertyIndexElement configPropElement = new ConfigPropertyIndexElement(name.getFullyQualifiedName(), fieldType.resolveBinding().getQualifiedName(), range);
135+
136+
beanDefinition.addChild(configPropElement);
137+
}
138+
}
139+
}
140+
} catch (BadLocationException e) {
141+
log.error("error identifying config property field", e);
142+
}
143+
}
144+
}
145+
146+
}
147+
148+
}

0 commit comments

Comments
 (0)