diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index 719f24ae1..71fb542d2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -41,9 +41,9 @@ import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine import org.springframework.ide.vscode.boot.java.handlers.BootJavaReferencesHandler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaWorkspaceSymbolHandler; import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider; +import org.springframework.ide.vscode.boot.java.handlers.CopilotCodeLensProvider; import org.springframework.ide.vscode.boot.java.handlers.HighlightProvider; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; -import org.springframework.ide.vscode.boot.java.handlers.CopilotCodeLensProvider; import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.livehover.ActiveProfilesProvider; @@ -319,6 +319,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent Map providers = new HashMap<>(); providers.put(Annotations.VALUE, new ValuePropertyReferencesProvider(projectFinder)); + providers.put(Annotations.CONDITIONAL_ON_PROPERTY, new ValuePropertyReferencesProvider(projectFinder)); providers.put(Annotations.QUALIFIER, new QualifierReferencesProvider(index, symbolIndex)); providers.put(Annotations.NAMED_JAKARTA, new NamedReferencesProvider(index, symbolIndex)); providers.put(Annotations.NAMED_JAVAX, new NamedReferencesProvider(index, symbolIndex)); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyExtractor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyExtractor.java new file mode 100644 index 000000000..eaff3cf18 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyExtractor.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * 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.value; + +import java.util.List; +import java.util.Map; + +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.Expression; +import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.LocationLink; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.commons.java.IJavaProject; + +public class PropertyExtractor { + + private static final String PARAM_VALUE = "value"; + private static final String PARAM_NAME = "name"; + private static final String PARAM_PREFIX = "prefix"; + + private static interface PropertyKeyExtractor { + String extract(Annotation annotation, MemberValuePair memberValuePair, StringLiteral stringLiteral); + } + + private final Map propertyKeyExtractors; + + public PropertyExtractor() { + propertyKeyExtractors = Map.of( + + Annotations.VALUE, (annotation, memberValuePair, stringLiteral) -> { + if (annotation.isSingleMemberAnnotation()) { + return extractPropertyKey(stringLiteral.getLiteralValue()); + } else if (annotation.isNormalAnnotation() && PARAM_VALUE.equals(memberValuePair.getName().getIdentifier())) { + return extractPropertyKey(stringLiteral.getLiteralValue()); + } + return null; + }, + + Annotations.CONDITIONAL_ON_PROPERTY, (annotation, memberValuePair, stringLiteral) -> { + if (annotation.isSingleMemberAnnotation()) { + return stringLiteral.getLiteralValue(); + } else if (annotation.isNormalAnnotation()) { + switch (memberValuePair.getName().getIdentifier()) { + case PARAM_VALUE: + return stringLiteral.getLiteralValue(); + case PARAM_NAME: + String prefix = extractAnnotationParameter(annotation, PARAM_PREFIX); + String name = stringLiteral.getLiteralValue(); + return prefix != null && !prefix.isBlank() ? prefix + "." + name : name; + } + } + return null; + } + ); + } + + public String extractPropertyKey(StringLiteral valueNode) { + + ASTNode parent = valueNode.getParent(); + + if (parent instanceof Annotation) { + + Annotation a = (Annotation) parent; + IAnnotationBinding binding = a.resolveAnnotationBinding(); + if (binding != null && binding.getAnnotationType() != null) { + PropertyKeyExtractor propertyExtractor = propertyKeyExtractors.get(binding.getAnnotationType().getQualifiedName()); + if (propertyExtractor != null) { + return propertyExtractor.extract(a, null, valueNode); + } + } + + } else if (parent instanceof MemberValuePair && parent.getParent() instanceof Annotation) { + + MemberValuePair pair = (MemberValuePair) parent; + Annotation a = (Annotation) parent.getParent(); + IAnnotationBinding binding = a.resolveAnnotationBinding(); + if (binding != null && binding.getAnnotationType() != null) { + PropertyKeyExtractor propertyExtractor = propertyKeyExtractors.get(binding.getAnnotationType().getQualifiedName()); + if (propertyExtractor != null) { + return propertyExtractor.extract(a, pair, valueNode); + } + } + } + + return null; + } + + private String extractPropertyKey(String s) { + if (s.length() > 3 && (s.startsWith("${") || s.startsWith("#{")) && s.endsWith("}")) { + return s.substring(2, s.length() - 1); + } + return null; + } + + @SuppressWarnings("unchecked") + private static String extractAnnotationParameter(Annotation a, String param) { + Expression value = null; + if (a.isSingleMemberAnnotation() && PARAM_VALUE.equals(param)) { + value = ((SingleMemberAnnotation) a).getValue(); + } else if (a.isNormalAnnotation()) { + for (MemberValuePair pair : (List) ((NormalAnnotation) a).values()) { + if (param.equals(pair.getName().getIdentifier())) { + value = pair.getValue(); + break; + } + } + } + if (value instanceof StringLiteral) { + return ((StringLiteral) value).getLiteralValue(); + } + return null; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java index 1558487cc..570ed80cc 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java @@ -18,13 +18,7 @@ import java.util.Map; import java.util.Optional; 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.Expression; -import org.eclipse.jdt.core.dom.IAnnotationBinding; -import org.eclipse.jdt.core.dom.MemberValuePair; -import org.eclipse.jdt.core.dom.NormalAnnotation; -import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; @@ -34,7 +28,6 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider; import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents; import org.springframework.ide.vscode.commons.java.IClasspathUtil; @@ -47,37 +40,12 @@ import com.google.common.collect.ImmutableList.Builder; public class ValueDefinitionProvider implements IJavaDefinitionProvider { private static final Logger log = LoggerFactory.getLogger(ValueDefinitionProvider.class); + private final PropertyExtractor propertyExtractor; - private static final String PARAM_VALUE = "value"; - private static final String PARAM_NAME = "name"; - private static final String PARAM_PREFIX = "prefix"; + public ValueDefinitionProvider() { + this.propertyExtractor = new PropertyExtractor(); + } - private Map annotationToPropertyKeyExtractor = Map.of( - Annotations.VALUE, (annotation, memberValuePair, stringLiteral) -> { - if (annotation.isSingleMemberAnnotation()) { - return extractPropertyKey(stringLiteral.getLiteralValue()); - } else if (annotation.isNormalAnnotation() && PARAM_VALUE.equals(memberValuePair.getName().getIdentifier())) { - return extractPropertyKey(stringLiteral.getLiteralValue()); - } - return null; - }, - Annotations.CONDITIONAL_ON_PROPERTY, (annotation, memberValuePair, stringLiteral) -> { - if (annotation.isSingleMemberAnnotation()) { - return stringLiteral.getLiteralValue(); - } else if (annotation.isNormalAnnotation()) { - switch (memberValuePair.getName().getIdentifier()) { - case PARAM_VALUE: - return stringLiteral.getLiteralValue(); - case PARAM_NAME: - String prefix = extractAnnotationParameter(annotation, PARAM_PREFIX); - String name = stringLiteral.getLiteralValue(); - return prefix != null && !prefix.isBlank() ? prefix + "." + name : name; - } - } - return null; - } - ); - @Override public List getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { @@ -99,30 +67,7 @@ public class ValueDefinitionProvider implements IJavaDefinitionProvider { } private List getDefinitionForProperty(IJavaProject project, CompilationUnit cu, StringLiteral valueNode) { - String propertyKey = null; - - ASTNode parent = valueNode.getParent(); - if (parent instanceof Annotation) { - Annotation a = (Annotation) parent; - IAnnotationBinding binding = a.resolveAnnotationBinding(); - if (binding != null && binding.getAnnotationType() != null) { - PropertyKeyExtractor propertyExtractor = annotationToPropertyKeyExtractor.get(binding.getAnnotationType().getQualifiedName()); - if (propertyExtractor != null) { - propertyKey = propertyExtractor.extract(a, null, valueNode); - } - } - } else if (parent instanceof MemberValuePair - && parent.getParent() instanceof Annotation) { - MemberValuePair pair = (MemberValuePair) parent; - Annotation a = (Annotation) parent.getParent(); - IAnnotationBinding binding = a.resolveAnnotationBinding(); - if (binding != null && binding.getAnnotationType() != null) { - PropertyKeyExtractor propertyExtractor = annotationToPropertyKeyExtractor.get(binding.getAnnotationType().getQualifiedName()); - if (propertyExtractor != null) { - propertyKey = propertyExtractor.extract(a, pair, valueNode); - } - } - } + String propertyKey = propertyExtractor.extractPropertyKey(valueNode); if (propertyKey != null) { Builder builder = ImmutableList.builder(); @@ -217,36 +162,6 @@ public class ValueDefinitionProvider implements IJavaDefinitionProvider { return links.build(); } - @SuppressWarnings("unchecked") - private static String extractAnnotationParameter(Annotation a, String param) { - Expression value = null; - if (a.isSingleMemberAnnotation() && PARAM_VALUE.equals(param)) { - value = ((SingleMemberAnnotation) a).getValue(); - } else if (a.isNormalAnnotation()) { - for (MemberValuePair pair : (List) ((NormalAnnotation) a).values()) { - if (param.equals(pair.getName().getIdentifier())) { - value = pair.getValue(); - break; - } - } - } - if (value instanceof StringLiteral) { - return ((StringLiteral) value).getLiteralValue(); - } - return null; - } - - private static String extractPropertyKey(String s) { - if (s.length() > 3 && (s.startsWith("${") || s.startsWith("#{")) && s.endsWith("}")) { - return s.substring(2, s.length() - 1); - } - return null; - } - - private interface PropertyKeyExtractor { - String extract(Annotation annotation, MemberValuePair memberValuePair, StringLiteral stringLiteral); - } - private List getDefinitionForClasspathResource(IJavaProject project, CompilationUnit cu, StringLiteral valueNode, String literalValue) { literalValue = literalValue.substring("classpath:".length()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyReferencesProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyReferencesProvider.java index c39af1805..09039a13e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyReferencesProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValuePropertyReferencesProvider.java @@ -31,7 +31,6 @@ import org.apache.commons.io.FileUtils; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; @@ -65,9 +64,11 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { private static final Logger log = LoggerFactory.getLogger(ValuePropertyReferencesProvider.class); private final JavaProjectFinder projectFinder; + private final PropertyExtractor propertyExtractor; public ValuePropertyReferencesProvider(JavaProjectFinder projectFinder) { this.projectFinder = projectFinder; + this.propertyExtractor = new PropertyExtractor(); } @Override @@ -76,34 +77,9 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { cancelToken.checkCanceled(); try { - // case: @Value("prefix<*>") - if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { - if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - return provideReferences(node.toString(), offset - node.getStartPosition(), node.getStartPosition()); - } - } - // case: @Value(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("\"")) { - return provideReferences(node.toString(), offset - node.getStartPosition(), node.getStartPosition()); - } - } - } - catch (Exception e) { - e.printStackTrace(); - } - - return null; - } - - private List provideReferences(String value, int offset, int nodeStartOffset) { - - try { - LocalRange range = getPropertyRange(value, offset); - if (range != null) { - String propertyKey = value.substring(range.getStart(), range.getEnd()); - if (propertyKey != null && propertyKey.length() > 0) { + if (node instanceof StringLiteral) { + String propertyKey = this.propertyExtractor.extractPropertyKey((StringLiteral) node); + if (propertyKey != null) { return findReferencesFromPropertyFiles(propertyKey); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalOnPropertyReferenceFinderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalOnPropertyReferenceFinderTest.java new file mode 100644 index 000000000..c5b515d99 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalOnPropertyReferenceFinderTest.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (c) 2024 Pivotal, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.conditionals.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.WorkspaceFolder; +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.java.Annotations; +import org.springframework.ide.vscode.boot.java.value.ValuePropertyReferencesProvider; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +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 ConditionalOnPropertyReferenceFinderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private String tempJavaDocUri; + + @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(); + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void testFindReferencesToPropertyFromAnnotation() throws Exception { + harness.getServer().getWorkspaceService().setWorkspaceFolders(List.of(new WorkspaceFolder(directory.toURI().toString()))); + + List references = getReferences("@ConditionalOnProperty(\"my.<*>prop\")"); + + assertEquals(1, references.size()); + + Location location = references.get(0); + assertEquals(directory.toPath().resolve("src/main/java/application.properties").toUri().toString(), location.getUri()); + assertEquals(0, location.getRange().getStart().getLine()); + assertEquals(0, location.getRange().getStart().getCharacter()); + assertEquals(0, location.getRange().getEnd().getLine()); + assertEquals(7, location.getRange().getEnd().getCharacter()); + } + + @Test + void testFindReferencesToPropertyFromAnnotationWithNameAttribute() throws Exception { + harness.getServer().getWorkspaceService().setWorkspaceFolders(List.of(new WorkspaceFolder(directory.toURI().toString()))); + + List references = getReferences("@ConditionalOnProperty(name = \"my.<*>prop\")"); + + assertEquals(1, references.size()); + + Location location = references.get(0); + assertEquals(directory.toPath().resolve("src/main/java/application.properties").toUri().toString(), location.getUri()); + assertEquals(0, location.getRange().getStart().getLine()); + assertEquals(0, location.getRange().getStart().getCharacter()); + assertEquals(0, location.getRange().getEnd().getLine()); + assertEquals(7, location.getRange().getEnd().getCharacter()); + } + + @Test + void testFindReferencesToPropertyFromAnnotationWithPrefix() throws Exception { + harness.getServer().getWorkspaceService().setWorkspaceFolders(List.of(new WorkspaceFolder(directory.toURI().toString()))); + + List references = getReferences("@ConditionalOnProperty(prefix=\"my\", name=\"<*>prop\")"); + + assertEquals(1, references.size()); + + Location location = references.get(0); + assertEquals(directory.toPath().resolve("src/main/java/application.properties").toUri().toString(), location.getUri()); + assertEquals(0, location.getRange().getStart().getLine()); + assertEquals(0, location.getRange().getStart().getCharacter()); + assertEquals(0, location.getRange().getEnd().getLine()); + assertEquals(7, location.getRange().getEnd().getCharacter()); + } + + private List getReferences(String completionLine) throws Exception { + harness.getServer().getWorkspaceService().setWorkspaceFolders(List.of(new WorkspaceFolder(directory.toURI().toString()))); + + String editorContent = """ + package org.test; + + import + """ + + Annotations.CONDITIONAL_ON_PROPERTY + ";" + + """ + + @Component + """ + + completionLine + "\n" + + """ + public class TestDependsOnClass { + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + return editor.getReferences(); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/references/test/ValuePropertyReferenceFinderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValuePropertyReferenceFinderTest.java similarity index 99% rename from headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/references/test/ValuePropertyReferenceFinderTest.java rename to headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValuePropertyReferenceFinderTest.java index 0cbc5960e..4f8c146c9 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/references/test/ValuePropertyReferenceFinderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValuePropertyReferenceFinderTest.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.references.test; +package org.springframework.ide.vscode.boot.java.value.test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull;