diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java index 874a0561a..7489c6461 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanUtils.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.Optional; import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.MethodDeclaration; @@ -33,7 +34,7 @@ public class BeanUtils { private static final String[] NAME_ATTRIBUTES = {"value", "name"}; - public static String getBeanNameFromComponentAnnotation(Annotation annotation, TypeDeclaration type) { + public static String getBeanNameFromComponentAnnotation(Annotation annotation, AbstractTypeDeclaration type) { Optional attribute = ASTUtils.getAttribute(annotation, "value"); if (attribute.isPresent()) { return ASTUtils.getExpressionValueAsString(attribute.get(), (a) -> {}); 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 4e0de93be..138b6c0dc 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 @@ -19,12 +19,14 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.RecordDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.SymbolKind; @@ -46,6 +48,7 @@ import org.springframework.ide.vscode.boot.java.utils.DefaultSymbolProvider; 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.DefaultValues; import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; import org.springframework.ide.vscode.commons.protocol.spring.SimpleSymbolElement; import org.springframework.ide.vscode.commons.util.BadLocationException; @@ -63,8 +66,11 @@ public class ComponentSymbolProvider implements SymbolProvider { @Override public void addSymbols(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) { try { - if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) { - createSymbol(node, annotationType, metaAnnotations, context, doc); + if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration type) { + createSymbol(type, node, annotationType, metaAnnotations, context, doc); + } + else if (node != null && node.getParent() != null && node.getParent() instanceof RecordDeclaration record) { + createSymbol(record, node, annotationType, metaAnnotations, context, doc); } else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) { WorkspaceSymbol symbol = DefaultSymbolProvider.provideDefaultSymbol(node, doc); @@ -77,15 +83,13 @@ public class ComponentSymbolProvider implements SymbolProvider { } } - protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException { + private void createSymbol(TypeDeclaration type, Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException { String annotationTypeName = annotationType.getName(); Collection metaAnnotationNames = metaAnnotations.stream() .map(ITypeBinding::getName) .collect(Collectors.toList()); - TypeDeclaration type = (TypeDeclaration) node.getParent(); - String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, type); ITypeBinding beanType = type.resolveBinding(); @@ -141,7 +145,48 @@ public class ComponentSymbolProvider implements SymbolProvider { context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition)); } - private void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) { + private void createSymbol(RecordDeclaration record, Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException { + String annotationTypeName = annotationType.getName(); + + Collection metaAnnotationNames = metaAnnotations.stream() + .map(ITypeBinding::getName) + .collect(Collectors.toList()); + + String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, record); + ITypeBinding beanType = record.resolveBinding(); + + Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength())); + + WorkspaceSymbol symbol = new WorkspaceSymbol( + beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface, + Either.forLeft(location)); + + boolean isConfiguration = Annotations.CONFIGURATION.equals(annotationType.getQualifiedName()) + || metaAnnotations.stream().anyMatch(t -> Annotations.CONFIGURATION.equals(t.getQualifiedName())); + + InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS; + + Set supertypes = new HashSet<>(); + ASTUtils.findSupertypes(beanType, supertypes); + + Collection annotationsOnType = ASTUtils.getAnnotations(record); + + AnnotationMetadata[] annotations = Stream.concat( + Arrays.stream(ASTUtils.getAnnotationsMetadata(annotationsOnType, doc)) + , + metaAnnotations.stream() + .map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null, null))) + .toArray(AnnotationMetadata[]::new); + + Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName()); + + indexConfigurationProperties(beanDefinition, record, context, doc); + + context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); + context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition)); + } + + private void indexConfigurationProperties(Bean beanDefinition, AbstractTypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) { AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type); if (annotationHierarchies.isAnnotatedWith(type.resolveBinding(), Annotations.CONFIGURATION_PROPERTIES)) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java index 6e69978b7..30b6e3f4e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java @@ -18,10 +18,13 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.FieldDeclaration; import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.RecordDeclaration; import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.VariableDeclarationFragment; @@ -55,8 +58,10 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider { @Override public void addSymbols(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) { try { - if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) { - createSymbol(node, annotationType, metaAnnotations, context, doc); + if (node != null && node.getParent() != null) { + if (node.getParent() instanceof AbstractTypeDeclaration abstractType) { + createSymbolForType(abstractType, node, annotationType, metaAnnotations, context, doc); + } } } catch (BadLocationException e) { @@ -64,14 +69,13 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider { } } - protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException { + protected void createSymbolForType(AbstractTypeDeclaration type, Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException { String annotationTypeName = annotationType.getName(); Collection metaAnnotationNames = metaAnnotations.stream() .map(ITypeBinding::getName) .collect(Collectors.toList()); - TypeDeclaration type = (TypeDeclaration) node.getParent(); ITypeBinding typeBinding = type.resolveBinding(); AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type); @@ -79,12 +83,11 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider { if (!isComponentAnnotated) { String beanName = BeanUtils.getBeanNameFromType(type.getName().getFullyQualifiedName()); - ITypeBinding beanType = type.resolveBinding(); Location location = new Location(doc.getUri(), doc.toRange(type.getStartPosition(), type.getLength())); WorkspaceSymbol symbol = new WorkspaceSymbol( - ComponentSymbolProvider.beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface, + ComponentSymbolProvider.beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, typeBinding.getName()), SymbolKind.Interface, Either.forLeft(location)); boolean isConfiguration = false; // otherwise, the ComponentSymbolProvider takes care of the bean definiton for this type @@ -92,7 +95,7 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider { InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc); Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(beanType, supertypes); + ASTUtils.findSupertypes(typeBinding, supertypes); Collection annotationsOnType = ASTUtils.getAnnotations(type); @@ -103,7 +106,7 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider { .map(an -> new AnnotationMetadata(an.getQualifiedName(), true, null, null))) .toArray(AnnotationMetadata[]::new); - Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName()); + Bean beanDefinition = new Bean(beanName, typeBinding.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration, symbol.getName()); indexConfigurationProperties(beanDefinition, type, context, doc); @@ -112,7 +115,16 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider { } } - public static void indexConfigurationProperties(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) { + public static void indexConfigurationProperties(Bean beanDefinition, AbstractTypeDeclaration abstractType, SpringIndexerJavaContext context, TextDocument doc) { + if (abstractType instanceof TypeDeclaration type) { + indexConfigurationPropertiesForType(beanDefinition, type, context, doc); + } + else if (abstractType instanceof RecordDeclaration record) { + indexConfigurationPropertiesForRecord(beanDefinition, record, context, doc); + } + } + + public static void indexConfigurationPropertiesForType(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) { FieldDeclaration[] fields = type.getFields(); if (fields != null) { @@ -145,4 +157,32 @@ public class ConfigurationPropertiesSymbolProvider implements SymbolProvider { } + public static void indexConfigurationPropertiesForRecord(Bean beanDefinition, RecordDeclaration record, SpringIndexerJavaContext context, TextDocument doc) { + + @SuppressWarnings("unchecked") + List fields = record.recordComponents(); + + if (fields != null) { + for (SingleVariableDeclaration field : fields) { + try { + Type fieldType = field.getType(); + if (fieldType != null) { + + SimpleName name = field.getName(); + if (name != null) { + + DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, field); + Range range = doc.toRange(nodeRegion); + ConfigPropertyIndexElement configPropElement = new ConfigPropertyIndexElement(name.getFullyQualifiedName(), fieldType.resolveBinding().getQualifiedName(), range); + + beanDefinition.addChild(configPropElement); + } + } + } catch (BadLocationException e) { + log.error("error identifying config property field", e); + } + } + } + + } } 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 169c8319e..c54998126 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 @@ -22,6 +22,7 @@ import java.util.function.Consumer; import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ArrayInitializer; import org.eclipse.jdt.core.dom.BodyDeclaration; @@ -39,6 +40,7 @@ import org.eclipse.jdt.core.dom.Modifier; import org.eclipse.jdt.core.dom.Name; import org.eclipse.jdt.core.dom.NormalAnnotation; import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.RecordDeclaration; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.eclipse.jdt.core.dom.SingleVariableDeclaration; @@ -284,10 +286,26 @@ public class ASTUtils { } } + public static Collection getAnnotations(AbstractTypeDeclaration abstractTypeDeclaration) { + if (abstractTypeDeclaration instanceof TypeDeclaration typeDeclaration) { + return getAnnotations(typeDeclaration); + } + else if (abstractTypeDeclaration instanceof RecordDeclaration recordDeclaration) { + return getAnnotations(recordDeclaration); + } + else { + return null; + } + } + public static Collection getAnnotations(TypeDeclaration typeDeclaration) { return getAnnotationsFromModifiers(typeDeclaration.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY)); } + public static Collection getAnnotations(RecordDeclaration recordDeclaration) { + return getAnnotationsFromModifiers(recordDeclaration.getStructuralProperty(RecordDeclaration.MODIFIERS2_PROPERTY)); + } + public static Collection getAnnotations(MethodDeclaration methodDeclaration) { return getAnnotationsFromModifiers(methodDeclaration.getStructuralProperty(MethodDeclaration.MODIFIERS2_PROPERTY)); } @@ -488,7 +506,16 @@ public class ASTUtils { return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS; } - public static InjectionPoint[] findInjectionPoints(TypeDeclaration type, TextDocument doc) throws BadLocationException { + public static InjectionPoint[] findInjectionPoints(AbstractTypeDeclaration abstractType, TextDocument doc) throws BadLocationException { + if (abstractType instanceof TypeDeclaration type) { + return findInjectionPointsForType(type, doc); + } + else { + return DefaultValues.EMPTY_INJECTION_POINTS; + } + } + + public static InjectionPoint[] findInjectionPointsForType(TypeDeclaration type, TextDocument doc) throws BadLocationException { List result = new ArrayList<>(); findInjectionPoints(type.getMethods(), doc, result); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerConfigurationPropertiesTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerConfigurationPropertiesTest.java index d2e4951a6..12fd9d1e1 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerConfigurationPropertiesTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerConfigurationPropertiesTest.java @@ -102,4 +102,51 @@ public class SpringIndexerConfigurationPropertiesTest { assertEquals("java.lang.String", configPropElement.getType()); } + @Test + void testSimpleConfigPropertiesRecord() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecords.java").toUri().toString(); + + Bean[] beans = springIndex.getBeansOfDocument(docUri); + assertEquals(1, beans.length); + + Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesWithRecords")).findFirst().get(); + assertEquals("com.example.configproperties.ConfigurationPropertiesWithRecords", configPropertiesComponentBean.getType()); + + List children = configPropertiesComponentBean.getChildren(); + assertEquals(2, children.size()); + + ConfigPropertyIndexElement configPropElement1 = (ConfigPropertyIndexElement) children.get(0); + assertEquals("name", configPropElement1.getName()); + assertEquals("java.lang.String", configPropElement1.getType()); + + ConfigPropertyIndexElement configPropElement2 = (ConfigPropertyIndexElement) children.get(1); + assertEquals("duration", configPropElement2.getName()); + assertEquals("int", configPropElement2.getType()); + } + + @Test + void testSimpleConfigPropertiesRecordAndConfigurationAnnotation() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecordsAndConfigurationAnnotation.java").toUri().toString(); + + Bean[] beans = springIndex.getBeansOfDocument(docUri); + assertEquals(1, beans.length); + + Bean configPropertiesComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("configurationPropertiesWithRecordsAndConfigurationAnnotation")).findFirst().get(); + assertEquals("com.example.configproperties.ConfigurationPropertiesWithRecordsAndConfigurationAnnotation", configPropertiesComponentBean.getType()); + + List children = configPropertiesComponentBean.getChildren(); + assertEquals(2, children.size()); + + ConfigPropertyIndexElement configPropElement1 = (ConfigPropertyIndexElement) children.get(0); + assertEquals("name", configPropElement1.getName()); + assertEquals("java.lang.String", configPropElement1.getType()); + + ConfigPropertyIndexElement configPropElement2 = (ConfigPropertyIndexElement) children.get(1); + assertEquals("duration", configPropElement2.getName()); + assertEquals("int", configPropElement2.getType()); + + } + + + } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesExampleWithConfigurationAnnotation.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesExampleWithConfigurationAnnotation.java index 9b875f61d..2b6dc11ae 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesExampleWithConfigurationAnnotation.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesExampleWithConfigurationAnnotation.java @@ -4,7 +4,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration -@ConfigurationProperties(prefix = "com.example.config.prefix.simple") +@ConfigurationProperties(prefix = "com.example.config.prefix.simple2") public class ConfigurationPropertiesExampleWithConfigurationAnnotation { private String simpleConfigProp = "default config value"; diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecordsAndConfigurationAnnotation.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecordsAndConfigurationAnnotation.java new file mode 100644 index 000000000..d9a05910b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-configuration-properties-indexing/src/main/java/com/example/configproperties/ConfigurationPropertiesWithRecordsAndConfigurationAnnotation.java @@ -0,0 +1,8 @@ +package com.example.configproperties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "com.example.config.record.prefix2") +public record ConfigurationPropertiesWithRecordsAndConfigurationAnnotation (String name, int duration) {}