diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java index ba1c59af0..4ffb7fba1 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2022 Pivotal, Inc. + * Copyright (c) 2018, 2023 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 @@ -14,6 +14,7 @@ import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -165,5 +166,21 @@ public class IClasspathUtil { }) .collect(CollectorUtil.toImmutableList()); } + + public static Stream getClasspathResourcesFullPaths(IClasspath classpath) { + return IClasspathUtil.getSourceFolders(classpath) + .flatMap(folder -> { + try { + return Files.walk(folder.toPath()) + .filter(path -> Files.isRegularFile(path)) + .filter(path -> { + String fileName = path.getFileName().toString(); + return !fileName.endsWith(".java") && !fileName.endsWith(".class"); + }); + } catch (IOException e) { + return Stream.empty(); + } + }); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java index b57e497c8..38809f841 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2020 Pivotal, Inc. + * Copyright (c) 2020, 2023 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 @@ -92,7 +92,7 @@ public class BootJavaCompletionEngineConfigurer { Map providers = new HashMap<>(); providers.put(org.springframework.ide.vscode.boot.java.scope.Constants.SPRING_SCOPE, new ScopeCompletionProcessor()); - providers.put(org.springframework.ide.vscode.boot.java.value.Constants.SPRING_VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties)); + providers.put(Annotations.VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties)); providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor()); return new BootJavaCompletionEngine(cuCache, providers, snippetManager); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index 8b2b8a147..d546f7cd9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -40,6 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.ide.vscode.boot.common.PropertyCompletionFactory; import org.springframework.ide.vscode.boot.common.RelaxedNameConfig; +import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaProjectReconcilerScheduler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; @@ -63,6 +64,7 @@ import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.utils.SymbolCache; import org.springframework.ide.vscode.boot.java.utils.SymbolCacheOnDisc; import org.springframework.ide.vscode.boot.java.utils.SymbolCacheVoid; +import org.springframework.ide.vscode.boot.java.value.PropertyValueAnnotationDefProvider; import org.springframework.ide.vscode.boot.jdt.ls.JavaProjectsService; import org.springframework.ide.vscode.boot.jdt.ls.JdtLsProjectCache; import org.springframework.ide.vscode.boot.metadata.AdHocSpringPropertyIndexProvider; @@ -361,4 +363,9 @@ public class BootLanguageServerBootApp { recipeRepoOpt.orElse(null), projectFinder, server); } + @Bean + JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { + return new JavaDefinitionHandler(cuCache, projectFinder, List.of(new PropertyValueAnnotationDefProvider())); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index c6c86bfe8..336a49568 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2022 Pivotal, Inc. + * Copyright (c) 2017, 2023 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 @@ -53,5 +53,7 @@ public class Annotations { public static final String CONDITIONAL_ON_JAVA = "org.springframework.boot.autoconfigure.condition.ConditionalOnJava"; public static final String CONDITIONAL_ON_JNDI = "org.springframework.boot.autoconfigure.condition.ConditionalOnJndi"; public static final String CONDITIONAL_ON_SINGLE_CANDIDATE = "org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate"; + + public static final String VALUE = "org.springframework.beans.factory.annotation.Value"; } 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 58aa9edef..380721b06 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 @@ -262,7 +262,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent BeanInjectedIntoHoverProvider beanInjectedIntoHoverProvider = new BeanInjectedIntoHoverProvider(sourceLinks); ConditionalsLiveHoverProvider conditionalsLiveHoverProvider = new ConditionalsLiveHoverProvider(); - providers.put(org.springframework.ide.vscode.boot.java.value.Constants.SPRING_VALUE, valueHoverProvider); + providers.put(Annotations.VALUE, valueHoverProvider); providers.put(Annotations.SPRING_REQUEST_MAPPING, requestMappingHoverProvider); providers.put(Annotations.SPRING_GET_MAPPING, requestMappingHoverProvider); @@ -301,7 +301,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent protected ReferencesHandler createReferenceHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder) { Map providers = new HashMap<>(); - providers.put(org.springframework.ide.vscode.boot.java.value.Constants.SPRING_VALUE, + providers.put(Annotations.VALUE, new ValuePropertyReferencesProvider(server)); return new BootJavaReferencesHandler(this, projectFinder, providers); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/IJavaDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/IJavaDefinitionProvider.java new file mode 100644 index 000000000..372279690 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/IJavaDefinitionProvider.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, 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: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java; + +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.commons.java.IJavaProject; + +public interface IJavaDefinitionProvider { + + List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JavaDefinitionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JavaDefinitionHandler.java new file mode 100644 index 000000000..b1e98d1d5 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JavaDefinitionHandler.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, 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: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; +import org.springframework.ide.vscode.commons.languageserver.util.LanguageSpecific; +import org.springframework.ide.vscode.commons.util.text.LanguageId; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +public class JavaDefinitionHandler implements DefinitionHandler, LanguageSpecific { + + private CompilationUnitCache cuCache; + private JavaProjectFinder projectFinder; + private Collection providers; + + public JavaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, + Collection providers) { + this.cuCache = cuCache; + this.projectFinder = projectFinder; + this.providers = providers; + } + + @Override + public Collection supportedLanguages() { + return Collections.singleton(LanguageId.JAVA); + } + + @Override + public List handle(CancelChecker cancelToken, DefinitionParams definitionParams) { + TextDocumentIdentifier doc = definitionParams.getTextDocument(); + IJavaProject project = projectFinder.find(doc).orElse(null); + if (project != null) { + URI docUri = URI.create(doc.getUri()); + return cuCache.withCompilationUnit(project, docUri, cu -> { + Builder builder = ImmutableList.builder(); + int start = cu.getPosition(definitionParams.getPosition().getLine() + 1, definitionParams.getPosition().getCharacter()); + ASTNode node = NodeFinder.perform(cu, start, 0); + for (IJavaDefinitionProvider provider : providers) { + if (cancelToken.isCanceled()) { + break; + } + builder.addAll(provider.getDefinitions(cancelToken, project, cu, node)); + } + return builder.build(); + }); + } + return Collections.emptyList(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java index 7401af35e..4e3fac700 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java @@ -26,9 +26,9 @@ import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.app.BootJavaConfig; +import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.handlers.SpelExpressionReconciler; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; -import org.springframework.ide.vscode.boot.java.value.Constants; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; @@ -66,8 +66,8 @@ public class JdtReconciler implements JavaReconciler { this.reconcilers = new AnnotationReconciler[] { - new AnnotationParamReconciler(Constants.SPRING_VALUE, null, "#{", "}", spelExpressionReconciler), - new AnnotationParamReconciler(Constants.SPRING_VALUE, "value", "#{", "}", spelExpressionReconciler), + new AnnotationParamReconciler(Annotations.VALUE, null, "#{", "}", spelExpressionReconciler), + new AnnotationParamReconciler(Annotations.VALUE, "value", "#{", "}", spelExpressionReconciler), new AnnotationParamReconciler(SPRING_CACHEABLE, "key", "", "", spelExpressionReconciler), new AnnotationParamReconciler(SPRING_CACHEABLE, "condition", "", "", spelExpressionReconciler), diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/Constants.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/Constants.java deleted file mode 100644 index 5dbb8c9c9..000000000 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/Constants.java +++ /dev/null @@ -1,20 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 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.value; - -/** - * @author Martin Lippert - */ -public class Constants { - - public static final String SPRING_VALUE = "org.springframework.beans.factory.annotation.Value"; - -} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyValueAnnotationDefProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyValueAnnotationDefProvider.java new file mode 100644 index 000000000..9d08e5ce5 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/PropertyValueAnnotationDefProvider.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, 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: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.value; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +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.FieldDeclaration; +import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +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; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.yaml.snakeyaml.nodes.Node; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; + +public class PropertyValueAnnotationDefProvider implements IJavaDefinitionProvider { + + private static final Logger log = LoggerFactory.getLogger(PropertyValueAnnotationDefProvider.class); + + @Override + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, + ASTNode n) { + if (n instanceof StringLiteral) { + StringLiteral valueNode = (StringLiteral) n; + String propertyKey = null; + + ASTNode parent = valueNode.getParent(); + if (parent instanceof Annotation && isApplicableValueAnnotation((Annotation) parent)) { + propertyKey = extractPropertyKey(valueNode.getLiteralValue()); + } else if (parent instanceof MemberValuePair + && "value".equals(((MemberValuePair) parent).getName().getIdentifier()) + && parent.getParent() instanceof Annotation + && isApplicableValueAnnotation((Annotation) parent.getParent())) { + propertyKey = extractPropertyKey(valueNode.getLiteralValue()); + } + + if (propertyKey != null) { + Builder builder = ImmutableList.builder(); + Map targetRanges = new HashMap<>(); + + Position startPosition = new Position(cu.getLineNumber(valueNode.getStartPosition()) - 1, + cu.getColumnNumber(valueNode.getStartPosition())); + Position endPosition = new Position( + cu.getLineNumber(valueNode.getStartPosition() + valueNode.getLength()) - 1, + cu.getColumnNumber(valueNode.getStartPosition() + valueNode.getLength())); + Range originRange = new Range(startPosition, endPosition); + + + for (Location location : findValueReferences(project, propertyKey, targetRanges)) { + LocationLink ll = new LocationLink(); + ll.setTargetUri(location.getUri()); + ll.setTargetSelectionRange(location.getRange()); + ll.setTargetRange(targetRanges.get(location)); + ll.setOriginSelectionRange(originRange); + builder.add(ll); + } + return builder.build(); + } + } + return Collections.emptyList(); + } + + private static boolean isApplicableValueAnnotation(Annotation a) { + IAnnotationBinding binding = a.resolveAnnotationBinding(); + return binding != null && Annotations.VALUE.equals(binding.getAnnotationType().getQualifiedName()) + && a.getParent() instanceof FieldDeclaration; + } + + private List findValueReferences(IJavaProject project, String propertyKey, Map targetRanges) { + Builder links = ImmutableList.builder(); + IClasspathUtil.getClasspathResourcesFullPaths(project.getClasspath()).forEach(path -> { + if (ValuePropertyReferencesProvider.isPropertiesFile(path)) { + String filePath = path.toString(); + if (filePath.endsWith(BootPropertiesLanguageServerComponents.PROPERTIES)) { + links.addAll(ValuePropertyReferencesProvider.findReferencesInPropertiesFile(path.toFile(), propertyKey, (pair, doc) -> { + try { + int line = doc.getLineOfOffset(pair.getValue().getOffset()); + int startInLine = pair.getValue().getOffset() - doc.getLineOffset(line); + int endInLine = startInLine + (pair.getValue().getLength()); + + Position start = new Position(); + start.setLine(line); + start.setCharacter(startInLine); + + Position end = new Position(); + end.setLine(line); + end.setCharacter(endInLine); + + Range range = new Range(); + range.setStart(start); + range.setEnd(end); + + Location location = new Location(path.toUri().toASCIIString(), range); + + targetRanges.put(location, doc.toRange(pair.getOffset(), pair.getLength())); + + return Optional.of(location); + } catch (Exception e) { + log.error("", e); + return Optional.empty(); + } + })); + } else { + for (String yml : BootPropertiesLanguageServerComponents.YML) { + if (filePath.endsWith(yml)) { + links.addAll(ValuePropertyReferencesProvider.findReferencesInYMLFile(path.toFile(), propertyKey, nodeTuple -> { + // property key node is found. Get the value node + Node valueNode = nodeTuple.getValueNode(); + Position valueStart = new Position(); + valueStart.setLine(valueNode.getStartMark().getLine()); + valueStart.setCharacter(valueNode.getStartMark().getColumn()); + + Position valueEnd = new Position(); + valueEnd.setLine(valueNode.getEndMark().getLine()); + valueEnd.setCharacter(valueNode.getEndMark().getColumn()); + + Range range = new Range(); + range.setStart(valueStart); + range.setEnd(valueEnd); + + Location location = new Location(path.toUri().toASCIIString(), new Range(valueStart, valueEnd)); + + Position keyStart = new Position(nodeTuple.getKeyNode().getStartMark().getLine(), nodeTuple.getKeyNode().getStartMark().getColumn()); + + targetRanges.put(location, new Range(keyStart, valueEnd)); + return Optional.of(location); + })); + } + } + } + } + }); + return links.build(); + } + + 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; + } + +} 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 04572dff1..1262a86f9 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2021 Pivotal, Inc. + * Copyright (c) 2017, 2023 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 @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.java.value; import static org.springframework.ide.vscode.commons.yaml.ast.NodeUtil.asScalar; import java.io.File; +import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; @@ -20,6 +21,9 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -34,10 +38,11 @@ import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; -import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.commons.yaml.ast.YamlASTProvider; import org.springframework.ide.vscode.commons.yaml.ast.YamlFileAST; @@ -55,6 +60,8 @@ import org.yaml.snakeyaml.nodes.NodeTuple; * @author Martin Lippert */ public class ValuePropertyReferencesProvider implements ReferenceProvider { + + private static final Logger log = LoggerFactory.getLogger(ValuePropertyReferencesProvider.class); private SimpleLanguageServer languageServer; @@ -133,7 +140,7 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { return null; } - private boolean isPropertiesFile(Path path) { + static boolean isPropertiesFile(Path path) { String fileName = path.getFileName().toString(); if (fileName.endsWith(BootPropertiesLanguageServerComponents.PROPERTIES)) { @@ -151,26 +158,46 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { private List findReferences(Path path, String propertyKey) { String filePath = path.toString(); if (filePath.endsWith(BootPropertiesLanguageServerComponents.PROPERTIES)) { - return findReferencesInPropertiesFile(filePath, propertyKey); + return findReferencesInPropertiesFile(path.toFile(), propertyKey); } else { for (String yml : BootPropertiesLanguageServerComponents.YML) { if (filePath.endsWith(yml)) { - return findReferencesInYMLFile(filePath, propertyKey); + return findReferencesInYMLFile(path.toFile(), propertyKey); } } } return new ArrayList(); } - private List findReferencesInYMLFile(String filePath, String propertyKey) { + private List findReferencesInYMLFile(File file, String propertyKey) { + + return findReferencesInYMLFile(file, propertyKey, foundNodeTuple -> { + Position start = new Position(); + Node foundNode = foundNodeTuple.getKeyNode(); + start.setLine(foundNode.getStartMark().getLine()); + start.setCharacter(foundNode.getStartMark().getColumn()); + + Position end = new Position(); + end.setLine(foundNode.getEndMark().getLine()); + end.setCharacter(foundNode.getEndMark().getColumn()); + + Range range = new Range(); + range.setStart(start); + range.setEnd(end); + + return Optional.of(new Location(file.toPath().toUri().toASCIIString(), range)); + }); + } + + static List findReferencesInYMLFile(File file, String propertyKey, Function> processor) { List foundLocations = new ArrayList<>(); try { - String fileContent = FileUtils.readFileToString(new File(filePath)); + String fileContent = FileUtils.readFileToString(file); YamlASTProvider parser = new YamlParser(); - URI docURI = Paths.get(filePath).toUri(); + URI docURI = file.toURI(); TextDocument doc = new TextDocument(docURI.toASCIIString(), null); doc.setText(fileContent); YamlFileAST ast = parser.getAST(doc); @@ -178,23 +205,13 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { List nodes = ast.getNodes(); if (nodes != null && !nodes.isEmpty()) { for (Node node : nodes) { - Node foundNode = findNode(node, "", propertyKey); - if (foundNode != null) { - - Position start = new Position(); - start.setLine(foundNode.getStartMark().getLine()); - start.setCharacter(foundNode.getStartMark().getColumn()); - - Position end = new Position(); - end.setLine(foundNode.getEndMark().getLine()); - end.setCharacter(foundNode.getEndMark().getColumn()); - - Range range = new Range(); - range.setStart(start); - range.setEnd(end); - - Location location = new Location(docURI.toASCIIString(), range); - foundLocations.add(location); + try { + NodeTuple foundNodeTuple = findNode(node, "", propertyKey); + if (foundNodeTuple != null) { + processor.apply(foundNodeTuple).ifPresent(foundLocations::add); + } + } catch (Exception e) { + log.error("", e); } } } @@ -206,8 +223,8 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { return foundLocations; } - - protected Node findNode(Node node, String prefix, String propertyKey) { + + protected static NodeTuple findNode(Node node, String prefix, String propertyKey) { if (node.getNodeId().equals(NodeId.mapping)) { for (NodeTuple entry : ((MappingNode)node).getValue()) { Node keyNode = entry.getKeyNode(); @@ -216,10 +233,10 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { String combinedKey = prefix.length() > 0 ? prefix + "." + key : key; if (combinedKey != null && combinedKey.equals(propertyKey)) { - return keyNode; + return entry; } else { - Node recursive = findNode(entry.getValueNode(), combinedKey, propertyKey); + NodeTuple recursive = findNode(entry.getValueNode(), combinedKey, propertyKey); if (recursive != null) { return recursive; } @@ -230,52 +247,59 @@ public class ValuePropertyReferencesProvider implements ReferenceProvider { return null; } - private List findReferencesInPropertiesFile(String filePath, String propertyKey) { + private List findReferencesInPropertiesFile(File file, String propertyKey) { + + return findReferencesInPropertiesFile(file, propertyKey, (pair, doc) -> { + try { + int line = doc.getLineOfOffset(pair.getKey().getOffset()); + int startInLine = pair.getKey().getOffset() - doc.getLineOffset(line); + int endInLine = startInLine + (pair.getKey().getLength()); + + Position start = new Position(); + start.setLine(line); + start.setCharacter(startInLine); + + Position end = new Position(); + end.setLine(line); + end.setCharacter(endInLine); + + Range range = new Range(); + range.setStart(start); + range.setEnd(end); + + return Optional.of(new Location(file.toPath().toUri().toASCIIString(), range)); + } catch (Exception e) { + log.error("", e); + return Optional.empty(); + } + }); + } + + static List findReferencesInPropertiesFile(File file, String propertyKey, BiFunction> processor) { List foundLocations = new ArrayList<>(); - try { - String fileContent = FileUtils.readFileToString(new File(filePath)); - + String fileContent = FileUtils.readFileToString(file); + Parser parser = new AntlrParser(); ParseResults parseResults = parser.parse(fileContent); - + if (parseResults != null && parseResults.ast != null) { parseResults.ast.getNodes(KeyValuePair.class).forEach(pair -> { if (pair.getKey() != null && pair.getKey().decode().equals(propertyKey)) { - URI docURI = Paths.get(filePath).toUri(); - TextDocument doc = new TextDocument(docURI.toASCIIString(), null); + TextDocument doc = new TextDocument(file.toURI().toASCIIString(), null); doc.setText(fileContent); - try { - int line = doc.getLineOfOffset(pair.getKey().getOffset()); - int startInLine = pair.getKey().getOffset() - doc.getLineOffset(line); - int endInLine = startInLine + (pair.getKey().getLength()); - - Position start = new Position(); - start.setLine(line); - start.setCharacter(startInLine); - - Position end = new Position(); - end.setLine(line); - end.setCharacter(endInLine); - - Range range = new Range(); - range.setStart(start); - range.setEnd(end); - - Location location = new Location(docURI.toASCIIString(), range); - foundLocations.add(location); - - } catch (BadLocationException e) { - e.printStackTrace(); + processor.apply(pair, doc).ifPresent(foundLocations::add); + } catch (Exception e) { + log.error("", e); } } }); } - - } catch (Exception e) { - e.printStackTrace(); + } catch (IOException e) { + log.error("", e); } + return foundLocations; } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/PropertyValueAnnotationDefProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/PropertyValueAnnotationDefProviderTest.java new file mode 100644 index 000000000..67d8470d3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/PropertyValueAnnotationDefProviderTest.java @@ -0,0 +1,146 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, 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: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.value.test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.junit.jupiter.api.AfterEach; +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.bootiful.AdHocPropertyHarnessTestConf; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.commons.java.IJavaProject; +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.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import({ AdHocPropertyHarnessTestConf.class, ValueCompletionTest.TestConf.class }) +public class PropertyValueAnnotationDefProviderTest { + + @Autowired + private BootLanguageServerHarness harness; + @Autowired + private IJavaProject testProject; + + private Set createdFiles = new HashSet<>(); + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + } + + @AfterEach + public void tearDown() throws Exception { + for (Path f : createdFiles) { + Files.deleteIfExists(f); + } + createdFiles.clear(); + } + + private Path projectFile(String relativePath, String content) throws IOException { + Path projectPath = Paths.get(testProject.getLocationUri()); + Path filePath = projectPath.resolve(relativePath); + Files.createDirectories(filePath.getParent()); + Files.write(filePath, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + createdFiles.add(filePath); + return filePath; + } + + @Test + void propertiesCase() throws Exception { + Path propertiesFilePath = projectFile("src/main/resources/application.properties", "some.prop=5"); + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.beans.factory.annotation.Value; + + public class TestValueCompletion { + + @Value("${some.prop}") + private String value1; + }"""); + + LocationLink expectedLocation = new LocationLink(propertiesFilePath.toUri().toASCIIString(), + new Range(new Position(0, 0), new Position(0, 11)), new Range(new Position(0, 10), new Position(0, 11)), + new Range(new Position(6, 8), new Position(6, 22))); + + editor.assertLinkTargets("some.prop", Set.of(expectedLocation)); + } + + @Test + void yamlCase() throws Exception { + Path yamlFilePath = projectFile("src/main/resources/application.yml", """ + some: + prop: 5 + """); + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.beans.factory.annotation.Value; + + public class TestValueCompletion { + + @Value("${some.prop}") + private String value1; + }"""); + + LocationLink expectedLocation = new LocationLink(yamlFilePath.toUri().toASCIIString(), + new Range(new Position(1, 2), new Position(1, 9)), new Range(new Position(1, 8), new Position(1, 9)), + new Range(new Position(6, 8), new Position(6, 22))); + + editor.assertLinkTargets("some.prop", Set.of(expectedLocation)); + } + + @Test + void combinedCase() throws Exception { + Path propertiesFilePath = projectFile("src/main/resources/application.properties", "some.prop=5"); + Path yamlFilePath = projectFile("src/main/resources/application.yml", """ + some: + prop: 5 + """); + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.beans.factory.annotation.Value; + + public class TestValueCompletion { + + @Value("${some.prop}") + private String value1; + }"""); + + LocationLink expectedPropsLocation = new LocationLink(propertiesFilePath.toUri().toASCIIString(), + new Range(new Position(0, 0), new Position(0, 11)), new Range(new Position(0, 10), new Position(0, 11)), + new Range(new Position(6, 8), new Position(6, 22))); + LocationLink expectedYamlLocation = new LocationLink(yamlFilePath.toUri().toASCIIString(), + new Range(new Position(1, 2), new Position(1, 9)), new Range(new Position(1, 8), new Position(1, 9)), + new Range(new Position(6, 8), new Position(6, 22))); + + editor.assertLinkTargets("some.prop", Set.of(expectedPropsLocation, expectedYamlLocation)); + + } +}