diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/BindingKeyUtils.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/BindingKeyUtils.java index 0ae8e6096..db64367d4 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/BindingKeyUtils.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/BindingKeyUtils.java @@ -45,6 +45,8 @@ class BindingKeyUtils { sb.append(getBindingKey(field.declaringClass())); sb.append('.'); sb.append(field.name()); + sb.append(')'); + sb.append(getGeneralTypeBindingKey(field.type())); return sb.toString(); } @@ -104,6 +106,9 @@ class BindingKeyUtils { sb.append(getGeneralTypeBindingKey(argument)); } sb.append('>'); + if (type.owner() == null) { + sb.append(';'); + } return sb.toString(); } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/TypeImpl.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/TypeImpl.java index c66b2d82c..9109099cc 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/TypeImpl.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/TypeImpl.java @@ -53,7 +53,7 @@ class TypeImpl implements IType { @Override public IType getDeclaringType() { DotName enclosingClass = info.enclosingClass(); - return enclosingClass == null ? null : index.findType(enclosingClass.toString()); + return enclosingClass == null ? this : index.findType(enclosingClass.toString()); } @Override diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/BootProjectUtil.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/BootProjectUtil.java index ff9227678..caffb50f8 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/BootProjectUtil.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/BootProjectUtil.java @@ -11,6 +11,8 @@ package org.springframework.ide.vscode.commons.java; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,4 +39,28 @@ public class BootProjectUtil { return name.endsWith(".jar") && name.startsWith("spring-boot"); } + public static Path javaHomeFromLibJar(Path libJar) { + for (Path home = libJar; home.getParent() != null; home = home.getParent()) { + if (Files.exists(home.resolve("release")) || Files.exists(home.resolve("release.txt"))) { + return home; + } + } + return null; + } + + public static Path jreSources(Path libJar) { + Path home = javaHomeFromLibJar(libJar); + if (home != null) { + Path sources = home.resolve("src.zip"); + if (Files.exists(sources)) { + return sources; + } + sources = home.resolve("lib/src.zip"); + if (Files.exists(sources)) { + return sources; + } + } + return null; + } + } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java index 03832c58d..c0778c3e5 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java @@ -21,6 +21,7 @@ import java.util.Set; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Resource; import org.apache.maven.project.MavenProject; +import org.springframework.ide.vscode.commons.java.BootProjectUtil; import org.springframework.ide.vscode.commons.java.ClasspathData; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.languageserver.jdt.ls.Classpath.CPE; @@ -86,6 +87,7 @@ public class MavenProjectClasspath implements IClasspath { if (javaVersion == null) { javaVersion = "8"; } + cpe.setSourceContainerUrl(BootProjectUtil.jreSources(path).toUri().toURL()); cpe.setJavadocContainerUrl(new URL("https://docs.oracle.com/javase/" + javaVersion + "/docs/api/")); cpe.setSystem(true); entries.add(cpe); diff --git a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java index 8afb9b453..06cf28bef 100644 --- a/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java +++ b/headless-services/commons/language-server-starter/src/main/java/org/springframework/ide/vscode/languageserver/starter/LanguageServerAutoConf.java @@ -21,7 +21,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerInitializer; import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties; import org.springframework.ide.vscode.commons.languageserver.reconcile.DiagnosticSeverityProvider; +import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; @Configuration @EnableConfigurationProperties(LanguageServerProperties.class) @@ -45,5 +47,14 @@ public class LanguageServerAutoConf { }; } - + @Bean SimpleTextDocumentService documents(SimpleLanguageServer ls) { + return ls.getTextDocumentService(); + } + + @ConditionalOnBean(DefinitionHandler.class) + @Bean + InitializingBean registerDefintionHandler(SimpleTextDocumentService documents, + DefinitionHandler definitionHandler) { + return () -> documents.onDefinition(definitionHandler); + } } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 1a171bcd6..209bd74d0 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -55,6 +55,7 @@ import org.springframework.ide.vscode.commons.util.Unicodes; import org.springframework.ide.vscode.commons.util.text.LanguageId; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import reactor.core.publisher.Flux; @@ -720,10 +721,28 @@ public class Editor { return "Editor(\n"+getText()+"\n)"; } + public void assertLinkTargets(String hoverOver, Set expectedLocations) throws Exception { + int pos = getRawText().indexOf(hoverOver); + if (pos>=0) { + pos += hoverOver.length() / 2; + } + assertTrue("Not found in editor: '"+hoverOver+"'", pos>=0); + + TextDocumentPositionParams params = new TextDocumentPositionParams(new TextDocumentIdentifier(getUri()), doc.toPosition(pos)); + List definitions = harness.getDefinitions(params); + + assertEquals(ImmutableSet.copyOf(expectedLocations), ImmutableSet.copyOf(definitions)); + } + + @Deprecated public void assertLinkTargets(String hoverOver, String... expecteds) { throw new UnsupportedOperationException("Not implemented yet!"); // Editor editor = this; -// int pos = editor.middleOf(hoverOver); +// int pos = getRawText().indexOf(hoverOver); +// if (pos>=0) { +// pos += hoverOver.length(); +// } +// return harness.getHover(doc, doc.toPosition(pos)); // assertTrue("Not found in editor: '"+hoverOver+"'", pos>=0); // // List targets = getLinkTargets(editor, pos); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java index 8b33b11e6..56838f0d2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguagServerBootApp.java @@ -14,7 +14,15 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.context.annotation.Bean; +import org.springframework.ide.vscode.boot.java.links.DefaultJavaElementLocationProvider; +import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; +import org.springframework.ide.vscode.boot.java.links.JavaDocumentUriProvider; +import org.springframework.ide.vscode.boot.java.links.JdtJavaDocumentUriProvider; +import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.util.LogRedirect; @SpringBootApplication @@ -34,4 +42,22 @@ public class BootLanguagServerBootApp { @Bean BootLanguageServerParams serverParams(SimpleLanguageServer server) { return BootLanguageServerParams.createDefault(server); } + + @ConditionalOnMissingClass("org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness") + @Bean SourceLinks sourceLinks(CompilationUnitCache cuCache) { + return SourceLinkFactory.createSourceLinks(cuCache); + } + + @Bean CompilationUnitCache cuCache(BootLanguageServerParams params, SimpleTextDocumentService documents) { + return new CompilationUnitCache(params.projectFinder, documents, params.projectObserver); + } + + @Bean JavaDocumentUriProvider javaDocumentUriProvider() { + return new JdtJavaDocumentUriProvider(); + } + + @Bean JavaElementLocationProvider javaElementLocationProvider(CompilationUnitCache cuCache, JavaDocumentUriProvider javaDocUriProvider) { + return new DefaultJavaElementLocationProvider(cuCache, javaDocUriProvider); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index 3af6fc1ea..017b06f8c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -15,6 +15,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.properties.BootPropertiesLanguageServerComponents; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter; @@ -32,6 +34,8 @@ public class BootLanguageServerInitializer implements InitializingBean { @Autowired SimpleLanguageServer server; @Autowired BootLanguageServerParams params; + @Autowired SourceLinks sourceLinks; + @Autowired CompilationUnitCache cuCache; private CompositeLanguageServerComponents components; private VscodeCompletionEngineAdapter completionEngineAdapter; @@ -55,7 +59,7 @@ public class BootLanguageServerInitializer implements InitializingBean { // some server intialization code. Migrate that code and get rid of the ComposableLanguageServer class CompositeLanguageServerComponents.Builder builder = new CompositeLanguageServerComponents.Builder(); builder.add(new BootPropertiesLanguageServerComponents(server, (ignore) -> params)); - builder.add(new BootJavaLanguageServerComponents(server, (ignore) -> params)); + builder.add(new BootJavaLanguageServerComponents(server, params, sourceLinks, cuCache)); components = builder.build(server); params.projectObserver.addListener(reconcileOpenDocuments(server, components)); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/PropertiesJavaDefinitionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/PropertiesJavaDefinitionHandler.java new file mode 100644 index 000000000..209422ad0 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/PropertiesJavaDefinitionHandler.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.app; + +import java.util.List; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.TextDocumentPositionParams; +import org.gradle.internal.impldep.com.google.common.collect.ImmutableList; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; +import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.boot.metadata.types.TypeUtil; +import org.springframework.ide.vscode.boot.properties.hover.PropertiesDefinitionCalculator; +import org.springframework.ide.vscode.commons.languageserver.util.DefinitionHandler; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.FuzzyMap; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.stereotype.Component; + +@Component +public class PropertiesJavaDefinitionHandler implements DefinitionHandler { + + @Autowired + private SimpleTextDocumentService documents; + + @Autowired + private JavaElementLocationProvider javaDocumentLocationProvider; + + @Autowired + private BootLanguageServerParams params; + + @Override + public List handle(TextDocumentPositionParams position) { + try { + TextDocument doc = documents.get(position); + TypeUtil typeUtil = params.typeUtilProvider.getTypeUtil(doc); + FuzzyMap index = params.indexProvider.getIndex(doc); + int offset; + offset = doc.toOffset(position.getPosition()); + return new PropertiesDefinitionCalculator(javaDocumentLocationProvider, index, typeUtil, doc, offset).calculate(); + } catch (BadLocationException e) { + return ImmutableList.of(); + } + } + +} 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 3d29d38c4..48a27e154 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,6 +41,7 @@ import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.livehover.ActiveProfilesProvider; import org.springframework.ide.vscode.boot.java.livehover.BeanInjectedIntoHoverProvider; import org.springframework.ide.vscode.boot.java.livehover.ComponentInjectionsHoverProvider; @@ -70,7 +71,6 @@ import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserve import org.springframework.ide.vscode.commons.languageserver.util.CodeLensHandler; import org.springframework.ide.vscode.commons.languageserver.util.DocumentHighlightHandler; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; -import org.springframework.ide.vscode.commons.languageserver.util.LSFactory; import org.springframework.ide.vscode.commons.languageserver.util.ReferencesHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; @@ -108,15 +108,15 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private CodeLensHandler codeLensHandler; private DocumentHighlightHandler highlightsEngine; - public BootJavaLanguageServerComponents(SimpleLanguageServer server, LSFactory _params) { + public BootJavaLanguageServerComponents(SimpleLanguageServer server, BootLanguageServerParams serverParams, SourceLinks sourceLinks, CompilationUnitCache cuCache) { this.server = server; - this.serverParams = _params.create(server); + this.serverParams = serverParams; this.config = new BootJavaConfig(); projectFinder = serverParams.projectFinder; projectObserver = serverParams.projectObserver; - cuCache = new CompilationUnitCache(projectFinder, server.getTextDocumentService(), projectObserver); + this.cuCache = cuCache; propertyIndexProvider = serverParams.indexProvider; adHocPropertyIndexProvider = serverParams.adHocIndexProvider; @@ -146,7 +146,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent // documents.onCodeLens(codeLensHandler::createCodeLenses); // documents.onCodeLensResolve(codeLensHandler::resolveCodeLens); - hoverProvider = createHoverHandler(projectFinder, serverParams.runningAppProvider); + hoverProvider = createHoverHandler(projectFinder, serverParams.runningAppProvider, sourceLinks); liveHoverWatchdog = new SpringLiveHoverWatchdog(server, hoverProvider, serverParams.runningAppProvider, projectFinder, projectObserver, serverParams.watchDogInterval); documents.onDidChangeContent(params -> { @@ -174,7 +174,14 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent // } }); - liveChangeDetectionWatchdog = new SpringLiveChangeDetectionWatchdog(this, server, serverParams.projectObserver, serverParams.runningAppProvider, projectFinder, serverParams.watchDogInterval); + liveChangeDetectionWatchdog = new SpringLiveChangeDetectionWatchdog( + this, + server, + serverParams.projectObserver, + serverParams.runningAppProvider, + projectFinder, + serverParams.watchDogInterval, + sourceLinks); codeLensHandler = createCodeLensEngine(); documents.onCodeLens(codeLensHandler); @@ -293,14 +300,14 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent } protected BootJavaHoverProvider createHoverHandler(JavaProjectFinder javaProjectFinder, - RunningAppProvider runningAppProvider) { + RunningAppProvider runningAppProvider, SourceLinks sourceLinks) { AnnotationHierarchyAwareLookup providers = new AnnotationHierarchyAwareLookup<>(); ValueHoverProvider valueHoverProvider = new ValueHoverProvider(); RequestMappingHoverProvider requestMappingHoverProvider = new RequestMappingHoverProvider(); - AutowiredHoverProvider autowiredHoverProvider = new AutowiredHoverProvider(this); - ComponentInjectionsHoverProvider componentInjectionsHoverProvider = new ComponentInjectionsHoverProvider(this); - BeanInjectedIntoHoverProvider beanInjectedIntoHoverProvider = new BeanInjectedIntoHoverProvider(this); + AutowiredHoverProvider autowiredHoverProvider = new AutowiredHoverProvider(sourceLinks); + ComponentInjectionsHoverProvider componentInjectionsHoverProvider = new ComponentInjectionsHoverProvider(sourceLinks); + BeanInjectedIntoHoverProvider beanInjectedIntoHoverProvider = new BeanInjectedIntoHoverProvider(sourceLinks); ConditionalsLiveHoverProvider conditionalsLiveHoverProvider = new ConditionalsLiveHoverProvider(); providers.put(org.springframework.ide.vscode.boot.java.value.Constants.SPRING_VALUE, valueHoverProvider); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index 97f4f6d13..3f293b3fc 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -32,10 +32,8 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.Annotations; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; -import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.livehover.ComponentInjectionsHoverProvider; import org.springframework.ide.vscode.boot.java.livehover.LiveHoverUtils; @@ -65,11 +63,11 @@ public class AutowiredHoverProvider implements HoverProvider { private static final int MAX_INLINE_BEANS_STRING_LENGTH = 60; private static final String INLINE_BEANS_STRING_SEPARATOR = " "; + private SourceLinks sourceLinks; - private BootJavaLanguageServerComponents server; + public AutowiredHoverProvider(SourceLinks sourceLinks) { + this.sourceLinks = sourceLinks; - public AutowiredHoverProvider(BootJavaLanguageServerComponents server) { - this.server = server; } @Override @@ -139,7 +137,7 @@ public class AutowiredHoverProvider implements HoverProvider { } else { hover.append(" \n \n"); } - createHoverContentForBeans(server, project, hover, autowiredBeans); + createHoverContentForBeans(sourceLinks, project, hover, autowiredBeans); hover.append("Bean id: `"); hover.append(definedBean.getId()); hover.append("` \n"); @@ -154,14 +152,13 @@ public class AutowiredHoverProvider implements HoverProvider { return null; } - public static void createHoverContentForBeans(BootJavaLanguageServerComponents server, IJavaProject project, StringBuilder hover, + public static void createHoverContentForBeans(SourceLinks sourceLinks, IJavaProject project, StringBuilder hover, List autowiredBeans) { - SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks(server); hover.append("**"); hover.append(LiveHoverUtils.createBeansTitleMarkdown(sourceLinks, project, autowiredBeans, BEANS_PREFIX_MARDOWN, MAX_INLINE_BEANS_STRING_LENGTH, INLINE_BEANS_STRING_SEPARATOR)); hover.append("**\n"); hover.append(autowiredBeans.stream() - .map(b -> "- " + LiveHoverUtils.showBeanWithResource(server, b, " ", project)) + .map(b -> "- " + LiveHoverUtils.showBeanWithResource(sourceLinks, b, " ", project)) .collect(Collectors.joining("\n"))); hover.append("\n \n"); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java index 3ab74121d..fe3688544 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java @@ -11,51 +11,41 @@ package org.springframework.ide.vscode.boot.java.links; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.Charset; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.Stack; -import org.apache.commons.io.IOUtils; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.EnumDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IClasspath; -import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.javadoc.TypeUrlProviderFromContainerUrl; import org.springframework.ide.vscode.commons.util.text.Region; -import org.springframework.ide.vscode.commons.util.text.TextDocument; - -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; /** - * Base logic for {@link SourceLinks} independent of any client - * + * Base logic for {@link SourceLinks} independent of any client + * * @author Alex Boyko * */ public abstract class AbstractSourceLinks implements SourceLinks { - - private static Supplier LOG = Suppliers.memoize(() -> LoggerFactory.getLogger(AbstractSourceLinks.class)); - - private BootJavaLanguageServerComponents server; - - protected AbstractSourceLinks(BootJavaLanguageServerComponents server) { - this.server = server; + + private static final Logger log = LoggerFactory.getLogger(AbstractSourceLinks.class); + + private CompilationUnitCache cuCache; + + protected AbstractSourceLinks(CompilationUnitCache cuCache) { + this.cuCache = cuCache; } @Override @@ -65,7 +55,7 @@ public abstract class AbstractSourceLinks implements SourceLinks { File file = classpathResource.get(); if (file.isDirectory()) { return javaSourceLinkUrl(project, fqName, file); - } else if (file.getName().endsWith(JAR)) { + } else { return jarSourceLinkUrl(project, fqName, file); } } @@ -81,123 +71,56 @@ public abstract class AbstractSourceLinks implements SourceLinks { } return Optional.empty(); } - + + + private Optional javaSourceLinkUrl(IJavaProject project, String fqName, File containerFolder) { IClasspath classpath = project.getClasspath(); - return IClasspathUtil.getSourceFolders(classpath) - .map(sourceFolder -> { - try { - return sourceFolder.toURI().toURL(); - } catch (MalformedURLException e) { - LOG.get().warn("Failed to convert source folder " + sourceFolder + "to URI." + fqName, e); - return null; - } - }) - .map(url -> { - try { - return TypeUrlProviderFromContainerUrl.SOURCE_FOLDER_URL_SUPPLIER.url(url, fqName); - } catch (Exception e) { - LOG.get().warn("Failed to determine source URL from url=" + url + " fqName=" + fqName, e); - return null; - } - }) - .map(url -> { - try { - return Paths.get(url.toURI()); - } catch (URISyntaxException e) { - LOG.get().warn("Failed to convert URL " + url + " to path." + fqName, e); - return null; - } - }) - .filter(sourcePath -> sourcePath != null && Files.exists(sourcePath)) - .findFirst() + return SourceLinks.sourceFromSourceFolder(fqName, classpath) .map(sourcePath -> javaSourceLinkUrl(project, sourcePath, fqName)); } private String javaSourceLinkUrl(IJavaProject project, Path sourcePath, String fqName) { Optional linkOptional = sourceLinkForResourcePath(sourcePath); if (linkOptional.isPresent()) { - Optional positionLink = findCUForJavaSourceFile(sourcePath).map(cu -> positionLink(cu, fqName)); + Optional positionLink = findCU(project, sourcePath.toUri()).map(cu -> positionLink(cu, fqName)); return positionLink.isPresent() ? linkOptional.get() + positionLink.get() : linkOptional.get(); } return null; } - + abstract protected String positionLink(CompilationUnit cu, String fqName); - - private Optional findCUForJavaSourceFile(Path resourcePath) { - Optional cu = findCUfromCache(resourcePath.toUri().toString()); - if (cu == null) { - try { - char[] bytes = new String(Files.readAllBytes(resourcePath), Charset.defaultCharset()).toCharArray(); - String uri = resourcePath.toUri().toString(); - String unitName = resourcePath.getFileName().toString(); - cu = Optional.ofNullable(CompilationUnitCache.parse(bytes, uri, unitName, new String[0])); - } catch (Exception e) { - LOG.get().warn("Failed to create CompilationUnit from " + resourcePath, e); - cu = Optional.empty(); - } - } - return cu; + + private Optional findCU(IJavaProject project, URI uri) { + return cuCache == null ? Optional.empty() : cuCache.withCompilationUnit(project, uri, compilationUnit -> Optional.ofNullable(compilationUnit)); } - - private Optional findCUfromCache(String uri) { - Optional cu = null; - if (server != null && server.getCompilationUnitCache() != null) { - TextDocument doc = server.getTextDocumentService().get(uri); - if (doc != null) { - cu = server.getCompilationUnitCache().withCompilationUnit(doc, compilationUnit -> compilationUnit == null ? null : Optional.of(compilationUnit)); - } - } - return cu; - } - - abstract protected Optional jarUrl(IJavaProject project, String fqName, File jarFile); - + + abstract protected Optional jarLinkUrl(IJavaProject project, String fqName, File jarFile); + private Optional jarSourceLinkUrl(IJavaProject project, String fqName, File jarFile) { - return jarUrl(project, fqName, jarFile).map(sourceUrl -> { - Optional positionLink = findCUForFQNameFromJar(project, jarFile, sourceUrl, fqName).map(cu -> positionLink(cu, fqName)); + return jarLinkUrl(project, fqName, jarFile).map(sourceUrl -> { + Optional positionLink = findCUForFQNameFromJar(project, jarFile, fqName).map(cu -> positionLink(cu, fqName)); return positionLink.isPresent() ? sourceUrl + positionLink.get() : sourceUrl; }); } - - private Optional findCUForFQNameFromJar(IJavaProject project, File jarFile, String clientSourceUri, String fqName) { - Optional cu = findCUfromCache(clientSourceUri); - if (cu == null) { - cu = project.sourceContainer(jarFile) - .map(url -> { - try { - return TypeUrlProviderFromContainerUrl.JAR_SOURCE_URL_PROVIDER.url(url, fqName); - } catch (Exception e) { - LOG.get().warn("Failed to determine source URL from url=" + url + " fqName=" + fqName, e); - return null; - } - }) - .map(sourceUrl -> { - InputStream openStream = null; - try { - openStream = sourceUrl.openStream(); - char[] bytes = IOUtils.toCharArray(openStream); - String uri = sourceUrl.toURI().toString(); - String unitName = fqName; - return CompilationUnitCache.parse(bytes, uri, unitName, new String[0]); - } catch (Exception e) { - LOG.get().warn("Failed to create CompilationUnit from " + sourceUrl, e); - return null; - } finally { - if (openStream != null) { - try { - openStream.close(); - } catch (IOException e) { - LOG.get().error("Failed to close stream from " + sourceUrl, e); - } - } - } - }); - } - return cu; + + private Optional findCUForFQNameFromJar(IJavaProject project, File jarFile, String fqName) { + return project.sourceContainer(jarFile).map(url -> { + try { + return TypeUrlProviderFromContainerUrl.JAR_SOURCE_URL_PROVIDER.url(url, fqName); + } catch (Exception e) { + log.warn("Failed to determine source URL from url={} fqName={}", url, fqName, e); + return null; + } + }).map(sourceUrl -> { + try { + return sourceUrl.toURI(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + }).map(sourcePath -> findCU(project, sourcePath).orElse(null)); } - + protected Region findTypeRegion(CompilationUnit cu, String fqName) { if (cu == null) { return null; @@ -209,7 +132,7 @@ public abstract class AbstractSourceLinks implements SourceLinks { if (packageName.equals(cu.getPackage().getName().getFullyQualifiedName())) { Stack visitedType = new Stack<>(); cu.accept(new ASTVisitor() { - + private boolean visitDeclaration(AbstractTypeDeclaration node) { visitedType.push(node.getName().getIdentifier()); if (values[1] < 0) { @@ -231,6 +154,17 @@ public abstract class AbstractSourceLinks implements SourceLinks { return visitDeclaration(node); } + @Override + public boolean visit(EnumDeclaration node) { + return visitDeclaration(node); + } + + @Override + public void endVisit(EnumDeclaration node) { + visitedType.pop(); + super.endVisit(node); + } + @Override public void endVisit(AnnotationTypeDeclaration node) { visitedType.pop(); @@ -247,5 +181,5 @@ public abstract class AbstractSourceLinks implements SourceLinks { } return values[1] < 0 ? null : new Region(values[0], values[1]); } - + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java index 81d3c973a..a4d4d6476 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/AtomSourceLinks.java @@ -19,7 +19,7 @@ import java.util.Optional; import org.eclipse.jdt.core.dom.CompilationUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.text.Region; @@ -28,7 +28,7 @@ import com.google.common.base.Suppliers; /** * Source links for Atom client - * + * * @author Alex Boyko * */ @@ -36,8 +36,8 @@ public class AtomSourceLinks extends AbstractSourceLinks { private static Supplier LOG = Suppliers.memoize(() -> LoggerFactory.getLogger(AbstractSourceLinks.class)); - public AtomSourceLinks(BootJavaLanguageServerComponents server) { - super(server); + public AtomSourceLinks(CompilationUnitCache cuCache) { + super(cuCache); } @Override @@ -69,7 +69,7 @@ public class AtomSourceLinks extends AbstractSourceLinks { } @Override - protected Optional jarUrl(IJavaProject project, String fqName, File jarFile) { + protected Optional jarLinkUrl(IJavaProject project, String fqName, File jarFile) { // JAR URLs are not supported yet return Optional.empty(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/DefaultJavaElementLocationProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/DefaultJavaElementLocationProvider.java new file mode 100644 index 000000000..e661d1a81 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/DefaultJavaElementLocationProvider.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.links; + +import java.net.URI; +import java.net.URL; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.EnumConstantDeclaration; +import org.eclipse.jdt.core.dom.EnumDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.commons.java.IField; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.IMember; +import org.springframework.ide.vscode.commons.java.IMethod; +import org.springframework.ide.vscode.commons.java.IType; + +public class DefaultJavaElementLocationProvider implements JavaElementLocationProvider { + + private static final Logger log = LoggerFactory.getLogger(DefaultJavaElementLocationProvider.class); + + private CompilationUnitCache cuCache; + private JavaDocumentUriProvider javaDocUriProvider; + + public DefaultJavaElementLocationProvider(CompilationUnitCache cuCache, JavaDocumentUriProvider javaDocUriProvider) { + this.cuCache = cuCache; + this.javaDocUriProvider = javaDocUriProvider; + } + + @Override + public Location findLocation(IJavaProject project, IMember member) { + Location loc = new Location(); + String fqName = member.getDeclaringType().getFullyQualifiedName(); + URI docUri = javaDocUriProvider.docUri(project, fqName); + if (docUri != null) { + loc.setUri(docUri.toString()); + Optional url = SourceLinks.source(project, fqName); + if (url.isPresent()) { + String memberBindingKey = member.getBindingKey(); + + try { + URI uri = url.get().toURI(); + Range r = cuCache.withCompilationUnit(project, uri, (cu) -> { + AtomicReference range = new AtomicReference<>(null); + cu.accept(new ASTVisitor() { + + private Range nameRange(SimpleName nameNode) { + int startOffset = nameNode.getStartPosition(); + int endOffset = nameNode.getLength() + startOffset; + + // Line -1 because for CU lines are starting from 1 + return new Range( + new Position(cu.getLineNumber(startOffset) - 1, cu.getColumnNumber(startOffset)), + new Position(cu.getLineNumber(endOffset) - 1, cu.getColumnNumber(endOffset))); + } + + @Override + public boolean visit(MethodDeclaration node) { + if (member instanceof IMethod) { + String bindingKey = node.resolveBinding().getKey(); + if (matchMethodBindingKeys(memberBindingKey, bindingKey)) { + range.set(nameRange(node.getName())); + return false; + } + } + return true; + } + + @Override + public boolean visit(EnumConstantDeclaration node) { + if (member instanceof IField) { + String bindingKey = node.resolveVariable().getKey(); + if (memberBindingKey.equals(bindingKey)) { + range.set(nameRange(node.getName())); + return false; + } + } + return true; + } + + @Override + public boolean visit(EnumDeclaration node) { + if (member instanceof IType) { + String bindingKey = node.resolveBinding().getKey(); + if (memberBindingKey.equals(bindingKey)) { + range.set(nameRange(node.getName())); + return false; + } + } + return true; + } + + @Override + public boolean visit(TypeDeclaration node) { + if (member instanceof IType) { + String bindingKey = node.resolveBinding().getKey(); + if (memberBindingKey.equals(bindingKey)) { + range.set(nameRange(node.getName())); + return false; + } + } + return true; + } + + }); + return range.get(); + }); + if (r == null) { + throw new IllegalStateException("Couldn't find " + member); + } + loc.setRange(r); + } catch (Throwable t) { + log.error("", t); + } + + } + } + return loc; + } + + private static boolean matchMethodBindingKeys(String key1, String key2) { + return removeReturnTypeFromMethodKeyBinding(key1).equals(removeReturnTypeFromMethodKeyBinding(key2)); + } + + private static String removeReturnTypeFromMethodKeyBinding(String key) { + int idx = key.lastIndexOf(')'); + return idx >= 0 ? key.substring(0, idx + 1) : key; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaDocumentUriProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaDocumentUriProvider.java new file mode 100644 index 000000000..9ecc4e331 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaDocumentUriProvider.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.links; + +import java.net.URI; + +import org.springframework.ide.vscode.commons.java.IJavaProject; + +@FunctionalInterface +public interface JavaDocumentUriProvider { + + URI docUri(IJavaProject project, String fqName); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaElementLocationProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaElementLocationProvider.java new file mode 100644 index 000000000..ed437fa3e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JavaElementLocationProvider.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.links; + +import org.eclipse.lsp4j.Location; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.IMember; + +@FunctionalInterface +public interface JavaElementLocationProvider { + + Location findLocation(IJavaProject project, IMember member); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JdtJavaDocumentUriProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JdtJavaDocumentUriProvider.java new file mode 100644 index 000000000..e6301d05a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/JdtJavaDocumentUriProvider.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.links; + +import java.io.File; +import java.net.URI; +import java.net.URLEncoder; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IJavaProject; + +public class JdtJavaDocumentUriProvider implements JavaDocumentUriProvider { + + private static final Logger log = LoggerFactory.getLogger(JdtJavaDocumentUriProvider.class); + + @Override + public URI docUri(IJavaProject project, String fqName) { + return uri(project, fqName); + } + + public static URI uri(IJavaProject project, String fqName) { + Optional classpathResource = project.getIndex().findClasspathResourceContainer(fqName); + if (classpathResource.isPresent()) { + File file = classpathResource.get(); + if (file.isDirectory()) { + IClasspath classpath = project.getClasspath(); + return SourceLinks.sourceFromSourceFolder(fqName, classpath).map(path -> path.toUri()).orElse(null); + } else { + try { + int lastDotIndex = fqName.lastIndexOf('.'); + String packageName = fqName.substring(0, lastDotIndex); + String typeName = fqName.substring(lastDotIndex + 1); + String jarFileName = file.getName(); + StringBuilder sb = new StringBuilder(); + sb.append("jdt://contents/"); + sb.append(jarFileName); + sb.append("/"); + sb.append(packageName); + sb.append("/"); + sb.append(typeName); + sb.append(SourceLinks.CLASS); + sb.append("?"); + + StringBuilder query = new StringBuilder(); + query.append("="); + query.append(project.getElementName()); + query.append("/"); + String convertedPath = file.toString().replace(File.separator, "\\/"); + query.append(convertedPath); + query.append("<"); + query.append(packageName); + query.append("("); + query.append(typeName); + query.append(SourceLinks.CLASS); + + sb.append(URLEncoder.encode(query.toString(), "UTF8")); + + return URI.create(sb.toString()); + } catch (Throwable t) { + log.warn("Failed creating Java document URI for " + file + " type " + fqName + " in the context of project " + project.getElementName(), t); + } + } + } + return null; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java index f33a4e5d2..0e12db838 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java @@ -14,54 +14,61 @@ import java.nio.file.Path; import java.util.Optional; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.util.LspClient; /** * Factory for creating {@link SourceLinks} - * + * * @author Alex Boyko * */ public final class SourceLinkFactory { - - private static final SourceLinks NO_SOURCE_LINKS = new SourceLinks() { - + + public static final SourceLinks NO_SOURCE_LINKS = new SourceLinks() { + @Override public Optional sourceLinkUrlForFQName(IJavaProject project, String fqName) { return Optional.empty(); } - + @Override public Optional sourceLinkUrlForClasspathResource(IJavaProject project, String path) { return Optional.empty(); } - + @Override public Optional sourceLinkForResourcePath(Path path) { return Optional.empty(); } - + }; - + /** * Creates {@link SourceLinks} for specific server based on client type * @param server the boot LS * @return appropriate source links object */ - public static SourceLinks createSourceLinks(BootJavaLanguageServerComponents server) { + public static SourceLinks createSourceLinks(CompilationUnitCache cuCache) { switch (LspClient.currentClient()) { case VSCODE: - case THEIA: - return new VSCodeSourceLinks(server); + case THEIA: + return new VSCodeSourceLinks(cuCache); case ECLIPSE: return new EclipseSourceLinks(); case ATOM: - return new AtomSourceLinks(server); + return new AtomSourceLinks(cuCache); default: return NO_SOURCE_LINKS; } } + @Deprecated + public static SourceLinks createSourceLinks(BootJavaLanguageServerComponents server) { + return server == null + ? createSourceLinks((CompilationUnitCache)null) + : createSourceLinks(server.getCompilationUnitCache()); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java index 43418fb8d..97db096ed 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java @@ -10,25 +10,97 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.links; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.javadoc.TypeUrlProviderFromContainerUrl; /** * Instance is able to provide client specific URL links to navigate to a * specific file on the client given various types of data such as fully * qualified java type name or classpath resource or just some file resource * given by its path. - * + * * @author Alex Boyko * */ public interface SourceLinks { - + + static final Logger log = LoggerFactory.getLogger(SourceLinks.class); + static final String JAR = ".jar"; static final String CLASS = ".class"; + public static Optional sourceFromSourceFolder(String fqName, IClasspath classpath) { + return IClasspathUtil.getSourceFolders(classpath) + .map(sourceFolder -> { + try { + return sourceFolder.toURI().toURL(); + } catch (MalformedURLException e) { + log.warn("Failed to convert source folder {} to URI {}", sourceFolder, fqName, e); + return null; + } + }) + .map(url -> { + try { + return TypeUrlProviderFromContainerUrl.SOURCE_FOLDER_URL_SUPPLIER.url(url, fqName); + } catch (Exception e) { + log.warn("Failed to determine source URL from url={} fqName=", url, fqName, e); + return null; + } + }) + .map(url -> { + try { + return Paths.get(url.toURI()); + } catch (URISyntaxException e) { + log.warn("Failed to convert URL {} to path. {}", url, fqName, e); + return null; + } + }) + .filter(sourcePath -> sourcePath != null && Files.exists(sourcePath)) + .findFirst(); + } + + public static Optional source(IJavaProject project, String fqName) { + Optional classpathResourceContainer = project.findClasspathResourceContainer(fqName); + // Try to find in a source JAR + Optional url = classpathResourceContainer + .flatMap(file -> project.sourceContainer(file)) + .map(file -> { + try { + return TypeUrlProviderFromContainerUrl.JAR_SOURCE_URL_PROVIDER.url(file, fqName); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + + + if (!url.isPresent()) { + // Try Source folder + url = classpathResourceContainer + .flatMap(file -> sourceFromSourceFolder(fqName, project.getClasspath()).map(p -> { + try { + return p.toUri().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + })); + } + return url; + } + + /** * Creates link to source file defining the type passed with it's fully qualified name * @param project Java project in the context of which source file link is calculated @@ -44,12 +116,12 @@ public interface SourceLinks { * @return the link URL optional */ Optional sourceLinkUrlForClasspathResource(IJavaProject project, String path); - + /** - * Creates link to a file specified by it's path + * Creates link to a file specified by it's path * @param path the resource path * @return the link URL optional */ Optional sourceLinkForResourcePath(Path path); - + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java index 22d2c3c4b..54eab45f3 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java @@ -11,32 +11,24 @@ package org.springframework.ide.vscode.boot.java.links; import java.io.File; -import java.net.URLEncoder; import java.nio.file.Path; import java.util.Optional; import org.eclipse.jdt.core.dom.CompilationUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.text.Region; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; - /** * VSCode specific source links implementation - * + * * @author Alex Boyko * */ public class VSCodeSourceLinks extends AbstractSourceLinks { - - private static Supplier LOG = Suppliers.memoize(() -> LoggerFactory.getLogger(AbstractSourceLinks.class)); - public VSCodeSourceLinks(BootJavaLanguageServerComponents server) { - super(server); + public VSCodeSourceLinks(CompilationUnitCache cuCache) { + super(cuCache); } @Override @@ -63,41 +55,8 @@ public class VSCodeSourceLinks extends AbstractSourceLinks { } @Override - protected Optional jarUrl(IJavaProject project, String fqName, File jarFile) { - try { - int lastDotIndex = fqName.lastIndexOf('.'); - String packageName = fqName.substring(0, lastDotIndex); - String typeName = fqName.substring(lastDotIndex + 1); - String jarFileName = jarFile.getName(); - StringBuilder sb = new StringBuilder(); - sb.append("jdt://contents/"); - sb.append(jarFileName); - sb.append("/"); - sb.append(packageName); - sb.append("/"); - sb.append(typeName); - sb.append(CLASS); - sb.append("?"); - - StringBuilder query = new StringBuilder(); - query.append("="); - query.append(project.getElementName()); - query.append("/"); - String convertedPath = jarFile.toString().replace(File.separator, "\\/"); - query.append(convertedPath); - query.append("<"); - query.append(packageName); - query.append("("); - query.append(typeName); - query.append(CLASS); - - sb.append(URLEncoder.encode(query.toString(), "UTF8")); - - return Optional.of(sb.toString()); - } catch (Throwable t) { - LOG.get().warn("Failed creating source URI for jar " + jarFile + " type " + fqName + " in the context of project " + project.getElementName(), t); - } - return Optional.empty(); + protected Optional jarLinkUrl(IJavaProject project, String fqName, File jarFile) { + return Optional.ofNullable(JdtJavaDocumentUriProvider.uri(project, fqName)).map(uri -> uri.toString()); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java index d580cc581..a5bab7e44 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java @@ -27,10 +27,8 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; -import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; @@ -51,10 +49,10 @@ public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider private static final int MAX_INLINE_BEANS_STRING_LENGTH = 60; private static final String INLINE_BEANS_STRING_SEPARATOR = " "; - protected BootJavaLanguageServerComponents server; + private SourceLinks sourceLinks; - public AbstractInjectedIntoHoverProvider(BootJavaLanguageServerComponents server) { - this.server = server; + public AbstractInjectedIntoHoverProvider(SourceLinks sourceLinks) { + this.sourceLinks = sourceLinks; } @Override @@ -149,18 +147,17 @@ public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider } if (!injectedBeans.isEmpty()) { - SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks(server); hover.append("**"); hover.append(LiveHoverUtils.createBeansTitleMarkdown(sourceLinks, project, injectedBeans, BEANS_PREFIX_MARKDOWN, MAX_INLINE_BEANS_STRING_LENGTH, INLINE_BEANS_STRING_SEPARATOR)); hover.append("**\n"); hover.append(injectedBeans.stream() - .map(b -> "- " + LiveHoverUtils.showBeanWithResource(server, b, " ", project)) + .map(b -> "- " + LiveHoverUtils.showBeanWithResource(sourceLinks, b, " ", project)) .collect(Collectors.joining("\n"))); hover.append("\n \n"); } List wiredBeans = findWiredBeans(project, app, relevantBeans, astNode); if (!wiredBeans.isEmpty()) { - AutowiredHoverProvider.createHoverContentForBeans(server, project, hover, wiredBeans); + AutowiredHoverProvider.createHoverContentForBeans(sourceLinks, project, hover, wiredBeans); } hover.append("Bean id: `"); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java index 0ccc773b3..c13ab5b42 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java @@ -17,8 +17,8 @@ import java.util.Optional; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.MethodDeclaration; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; @@ -27,8 +27,8 @@ import org.springframework.ide.vscode.commons.util.Optionals; public class BeanInjectedIntoHoverProvider extends AbstractInjectedIntoHoverProvider { - public BeanInjectedIntoHoverProvider(BootJavaLanguageServerComponents server) { - super(server); + public BeanInjectedIntoHoverProvider(SourceLinks sourceLinks) { + super(sourceLinks); } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java index 981a45c00..76d555d63 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java @@ -28,8 +28,8 @@ import org.eclipse.lsp4j.Range; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.Annotations; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; @@ -44,8 +44,8 @@ public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverP private static Logger LOG = LoggerFactory.getLogger(ComponentInjectionsHoverProvider.class); - public ComponentInjectionsHoverProvider(BootJavaLanguageServerComponents server) { - super(server); + public ComponentInjectionsHoverProvider(SourceLinks sourceLinks) { + super(sourceLinks); } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java index 74298bb0a..75c0ba6e3 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java @@ -20,8 +20,6 @@ import java.util.stream.Collectors; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.Range; -import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; -import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.SpringResource; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; @@ -47,7 +45,7 @@ public class LiveHoverUtils { return buf.toString(); } - public static String showBeanWithResource(BootJavaLanguageServerComponents server, LiveBean bean, String indentStr, IJavaProject project) { + public static String showBeanWithResource(SourceLinks sourceLinks, LiveBean bean, String indentStr, IJavaProject project) { String newline = " \n"+indentStr; //Note: the double space before newline makes markdown see it as a real line break if (bean == CANT_MATCH_PROPER_BEAN) { @@ -59,7 +57,6 @@ public class LiveHoverUtils { buf.append('`'); buf.append(bean.getId()); buf.append('`'); - SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks(server); if (type != null) { // Try creating a URL link to open source for the type buf.append(newline); @@ -126,33 +123,6 @@ public class LiveHoverUtils { return true; } - public static String showBeanIdAndTypeInline(BootJavaLanguageServerComponents server, IJavaProject project, LiveBean bean) { - String id = bean.getId(); - String type = bean.getType(true); - SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks(server); - String displayType = type; - if (type != null) { - int lastDotIdx = type.lastIndexOf('.'); - if (lastDotIdx >= 0 && lastDotIdx < type.length() - 1) { - displayType = "`" + type.substring(lastDotIdx + 1) + "`"; - } - Optional url = sourceLinks.sourceLinkUrlForFQName(project, type); - if (url.isPresent()) { - displayType = Renderables.link(displayType, url.get()).toMarkdown(); - } - } - StringBuilder sb = new StringBuilder(); - sb.append('`'); - sb.append(id); - sb.append('`'); - if (displayType != null) { - sb.append(' '); - sb.append(displayType); - } - return sb.toString(); - } - - public static String showResource(SourceLinks sourceLinks, String resource, IJavaProject project) { return new SpringResource(sourceLinks, resource, project).toMarkdown(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java index 5ab5ea222..0a39bc172 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -22,23 +22,27 @@ import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.function.Function; import java.util.stream.Stream; +import org.apache.commons.io.IOUtils; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; -import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -public final class CompilationUnitCache { +public final class CompilationUnitCache implements DocumentContentProvider { + + private static final Logger logger = LoggerFactory.getLogger(CompilationUnitCache.class); private static final long CU_ACCESS_EXPIRATION = 1; private JavaProjectFinder projectFinder; @@ -46,13 +50,15 @@ public final class CompilationUnitCache { private Cache uriToCu; private Cache> projectToDocs; private ProjectObserver.Listener projectListener; + private SimpleTextDocumentService documents; private ReadLock readLock; private WriteLock writeLock; - public CompilationUnitCache(JavaProjectFinder projectFinder, SimpleTextDocumentService documentService, ProjectObserver projectObserver) { + public CompilationUnitCache(JavaProjectFinder projectFinder, SimpleTextDocumentService documents, ProjectObserver projectObserver) { this.projectFinder = projectFinder; this.projectObserver = projectObserver; + this.documents = documents; projectListener = ProjectObserver.onAny(this::invalidateProject); // PT 154618835 - Avoid retaining the CU in the cache as it consumes memory if it hasn't been @@ -66,9 +72,9 @@ public final class CompilationUnitCache { readLock = lock.readLock(); writeLock = lock.writeLock(); - if (documentService != null) { - documentService.onDidChangeContent(doc -> invalidateCuForJavaFile(doc.getDocument().getId().getUri())); - documentService.onDidClose(doc -> invalidateCuForJavaFile(doc.getId().getUri())); + if (documents != null) { + documents.onDidChangeContent(doc -> invalidateCuForJavaFile(doc.getDocument().getId().getUri())); + documents.onDidClose(doc -> invalidateCuForJavaFile(doc.getId().getUri())); } if (this.projectObserver != null) { @@ -90,10 +96,14 @@ public final class CompilationUnitCache { * not pass of AST nodes to helper functions that work aynchronously or store AST nodes or ITypeBindings * for later use. The JDT ASTs are not thread safe! */ + @Deprecated public T withCompilationUnit(TextDocument document, Function requestor) { - URI uri = URI.create(document.getUri()); IJavaProject project = projectFinder.find(document.getId()).orElse(null); + URI uri = URI.create(document.getUri()); + return withCompilationUnit(project, uri, requestor); + } + public T withCompilationUnit(IJavaProject project, URI uri, Function requestor) { if (project != null) { readLock.lock(); @@ -101,15 +111,15 @@ public final class CompilationUnitCache { try { cu = uriToCu.get(uri, () -> { - CompilationUnit cUnit = parse(document, project); - projectToDocs.get(project, () -> new HashSet<>()).add(URI.create(document.getUri())); + CompilationUnit cUnit = parse(uri.toString(), fetchContent(uri).toCharArray(), project); + projectToDocs.get(project, () -> new HashSet<>()).add(uri); return cUnit; }); if (cu != null) { - projectToDocs.get(project, () -> new HashSet<>()).add(URI.create(document.getUri())); + projectToDocs.get(project, () -> new HashSet<>()).add(uri); } } catch (Exception e) { - Log.log(e); + logger.error("", e); } finally { readLock.unlock(); } @@ -121,7 +131,7 @@ public final class CompilationUnitCache { } } catch (Exception e) { - Log.log(e); + logger.error("", e); } } } @@ -129,6 +139,7 @@ public final class CompilationUnitCache { return requestor.apply(null); } + private void invalidateCuForJavaFile(String uriStr) { URI uri = URI.create(uriStr); writeLock.lock(); @@ -140,13 +151,19 @@ public final class CompilationUnitCache { } public static CompilationUnit parse(TextDocument document, IJavaProject project) throws Exception { - String[] classpathEntries = getClasspathEntries(document, project); + String[] classpathEntries = getClasspathEntries(project); String docURI = document.getUri(); String unitName = docURI.substring(docURI.lastIndexOf("/")); char[] source = document.get(0, document.getLength()).toCharArray(); return parse(source, docURI, unitName, classpathEntries); } - + + public static CompilationUnit parse(String uri, char[] source, IJavaProject project) throws Exception { + String[] classpathEntries = getClasspathEntries(project); + String unitName = uri.substring(uri.lastIndexOf("/")); + return parse(source, uri, unitName, classpathEntries); + } + public static CompilationUnit parse(char[] source, String docURI, String unitName, String[] classpathEntries) throws Exception { ASTParser parser = ASTParser.newParser(AST.JLS10); Map options = JavaCore.getOptions(); @@ -168,7 +185,7 @@ public final class CompilationUnitCache { return cu; } - private static String[] getClasspathEntries(TextDocument document, IJavaProject project) throws Exception { + private static String[] getClasspathEntries(IJavaProject project) throws Exception { if (project == null) { return new String[0]; } else { @@ -192,4 +209,16 @@ public final class CompilationUnitCache { } } } + + @Override + public String fetchContent(URI uri) throws Exception { + if (documents != null) { + TextDocument document = documents.get(uri.toString()); + if (document != null) { + return document.get(0, document.getLength()); + } + } + return IOUtils.toString(uri); + + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/DocumentContentProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/DocumentContentProvider.java new file mode 100644 index 000000000..1b9375f2e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/DocumentContentProvider.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import java.net.URI; + +@FunctionalInterface +public interface DocumentContentProvider { + + String fetchContent(URI uri) throws Exception; + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveChangeDetectionWatchdog.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveChangeDetectionWatchdog.java index 7314408f3..095120b96 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveChangeDetectionWatchdog.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringLiveChangeDetectionWatchdog.java @@ -35,7 +35,6 @@ import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents import org.springframework.ide.vscode.boot.java.handlers.RunningAppMatcher; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.boot.java.links.SourceLinks; -import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -72,7 +71,8 @@ public class SpringLiveChangeDetectionWatchdog { ProjectObserver projectObserver, RunningAppProvider runningAppProvider, JavaProjectFinder projectFinder, - Duration pollingInterval + Duration pollingInterval, + SourceLinks sourceLinks ) { this.observedProjects = new HashSet<>(); @@ -82,7 +82,7 @@ public class SpringLiveChangeDetectionWatchdog { this.POLLING_INTERVAL_MILLISECONDS = pollingInterval == null ? DEFAULT_INTERVAL.toMillis() : pollingInterval.toMillis(); this.changeHistory = new ChangeDetectionHistory(); - this.sourceLinks = new VSCodeSourceLinks(bootJavaLanguageServerComponents); + this.sourceLinks = sourceLinks; if (projectObserver != null) { projectObserver.addListener(new Listener() { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/hints/StsValueHint.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/hints/StsValueHint.java index 5d01a2086..48edecf36 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/hints/StsValueHint.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/hints/StsValueHint.java @@ -15,6 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.configurationmetadata.Deprecation; import org.springframework.ide.vscode.boot.configurationmetadata.ValueHint; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.metadata.types.TypeUtil; @@ -120,10 +121,10 @@ public class StsValueHint { public Renderable getDescription() { return description; } - + private static Renderable javaDocSnippet(IJavaProject project, IJavaElement je) { return Renderables.lazy(() -> { - SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks(null); + SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks((BootJavaLanguageServerComponents)null); return PropertyDocUtils.documentJavaElement(sourceLinks, project, je); }); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java index ce73a5e29..57b7bd094 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/types/TypeUtil.java @@ -37,6 +37,7 @@ import javax.inject.Provider; import org.springframework.boot.convert.DurationStyle; import org.springframework.ide.vscode.boot.configurationmetadata.Deprecation; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.metadata.ResourceHintProvider; @@ -657,7 +658,7 @@ public class TypeUtil { //TODO: handle type parameters. if (typeFromIndex != null) { - SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks(null); + SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks((BootJavaLanguageServerComponents)null); IJavaProject project = getJavaProject(); ArrayList properties = new ArrayList<>(); getGetterMethods(typeFromIndex).forEach(m -> { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/util/PropertyDocUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/util/PropertyDocUtils.java index 65b8c0a56..13bea2162 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/util/PropertyDocUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/util/PropertyDocUtils.java @@ -12,7 +12,6 @@ package org.springframework.ide.vscode.boot.metadata.util; import java.util.Optional; -import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.commons.java.IJavaElement; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -27,15 +26,15 @@ import com.google.common.collect.ImmutableList.Builder; /** * Boot properties documentation info utils - * + * * @author Alex Boyko * */ public class PropertyDocUtils { - + /** * Generates documentation for boot property coming from java element - * + * * @param sourceLinks * @param project * @param je @@ -48,10 +47,10 @@ public class PropertyDocUtils { renderableBuilder.add(Renderables.paragraph(javadoc == null ? Renderables.NO_DESCRIPTION: javadoc.getRenderable())); return Renderables.concat(renderableBuilder.build()); } - + /** * Generates documentation for the value of some Java type. Includes signature, javadoc, link to container type. - * + * * @param sourceLinks * @param project * @param je @@ -66,7 +65,7 @@ public class PropertyDocUtils { IType containingType = je instanceof IType ? (IType) je : ((IMember)je).getDeclaringType(); if (je != null) { String type = containingType.getFullyQualifiedName(); - Optional url = SourceLinkFactory.createSourceLinks(null).sourceLinkUrlForFQName(project, type); + Optional url = sourceLinks.sourceLinkUrlForFQName(project, type); renderableBuilder.add(Renderables.lineBreak()); if (url.isPresent()) { renderableBuilder.add(Renderables.link(type, url.get())); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertiesDefinitionCalculator.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertiesDefinitionCalculator.java new file mode 100644 index 000000000..6338d7cac --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/hover/PropertiesDefinitionCalculator.java @@ -0,0 +1,237 @@ +/******************************************************************************* + * Copyright (c) 2018 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.properties.hover; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.lsp4j.Location; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.links.JavaElementLocationProvider; +import org.springframework.ide.vscode.boot.metadata.PropertyInfo; +import org.springframework.ide.vscode.boot.metadata.PropertyInfo.PropertySource; +import org.springframework.ide.vscode.boot.metadata.types.TypeUtil; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.IMember; +import org.springframework.ide.vscode.commons.java.IMethod; +import org.springframework.ide.vscode.commons.java.IType; +import org.springframework.ide.vscode.commons.util.FuzzyMap; +import org.springframework.ide.vscode.commons.util.StringUtil; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Key; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value; + +import com.google.common.collect.ImmutableList; + +public class PropertiesDefinitionCalculator { + + private static final Logger log = LoggerFactory.getLogger(PropertiesDefinitionCalculator.class); + + final private PropertyFinder propertyFinder; + final private IJavaProject project; + final private JavaElementLocationProvider javaElementLocationProvider; + + public PropertiesDefinitionCalculator(JavaElementLocationProvider javaElementLocationProvider, FuzzyMap index, TypeUtil typeUtil, IDocument doc, int offset) { + this.javaElementLocationProvider = javaElementLocationProvider; + this.propertyFinder = new PropertyFinder(index, typeUtil, doc, offset); + this.project = typeUtil.getJavaProject(); + } + + public List calculate() { + Node node = propertyFinder.findNode(); + if (node instanceof Key) { + Collection propertyJavaElements = getPropertyJavaElements(((Key) node).decode()); + return getPropertyDefintion(propertyJavaElements); + } else if (node instanceof Value) { + Value value = (Value) node; + Key key = value.getParent().getKey(); + + String javaTypeFqName = getPropertyJavaTypeFqName(key.decode()); + + if (javaTypeFqName != null) { + + // Class reference value link + if ("java.lang.Class".equals(javaTypeFqName)) { + IType javaType = project.findType(value.decode()); + if (javaType != null) { + Location location = findLocation(javaType); + if (location != null) { + return ImmutableList.of(location); + } + } + } + + IType javaType = project.findType(javaTypeFqName); + if (javaType != null) { + // Enum value link + if (javaType.isEnum()) { + String enumValue = StringUtil.hyphensToUpperCase(StringUtil.camelCaseToHyphens(value.decode())); + ImmutableList.Builder list = ImmutableList.builder(); + javaType.getFields().forEach(field -> { + if (field.getElementName().equals(enumValue)) { + Location location = findLocation(field); + if (location != null) { + list.add(location); + } + } + }); + return list.build(); + } + } + + } + + + } + return ImmutableList.of(); + } + + private List getPropertyDefintion(Collection propertyJavaElements) { + return propertyJavaElements.stream().map(this::findLocation).filter(Objects::nonNull).collect(Collectors.toList()); + } + + private Location findLocation(IMember element) { + return javaElementLocationProvider.findLocation(project, element); + } + + private String getPropertyJavaTypeFqName(String propertyKey) { + PropertyInfo best = propertyFinder.findBestHoverMatch(propertyKey); + if (best != null) { + String type = best.getType(); + // Trim down generic type if present + int idx = type == null ? -1 : type.indexOf('<'); + return idx < 0 ? type : type.substring(0, idx); + } + return null; + } + + private Collection getPropertyJavaElements(String propertyKey) { + PropertyInfo best = propertyFinder.findBestHoverMatch(propertyKey); + if (best != null) { + List sources = best.getSources(); + if (sources != null) { + ImmutableList.Builder elements = ImmutableList.builder(); + for (PropertySource source : sources) { + String typeName = source.getSourceType(); + if (typeName!=null) { + IType type = project.findType(typeName); + IMethod method = null; + if (type!=null) { + String methodSig = source.getSourceMethod(); + if (methodSig!=null) { + method = getMethod(type, methodSig); + } else { + method = getSetter(type, best); + } + } + if (method!=null) { + elements.add(method); + } else if (type!=null) { + elements.add(type); + } + } + } + return elements.build(); + } + } + return ImmutableList.of(); + } + + private IMethod getMethod(IType type, String methodSig) { + String name = getMethodName(methodSig); + //TODO: This code assumes 0 arguments, which is the case currently for all + // 'real' data in spring jars. + IMethod m = type.getMethod(name, Stream.empty()); + if (m!=null) { + return m; + } + //try find a method with the same name. + return type.getMethods() + .filter(meth -> name.equals(meth.getElementName())) + .findFirst() + .orElse(null); + } + + private static String getMethodName(String methodSig) { + String name; + int nameEnd = methodSig.indexOf('('); + if (nameEnd>=0) { + name = methodSig.substring(0, nameEnd); + int space = name.lastIndexOf(' '); + if (space >= 0) { + name = name.substring(space + 1); + } + } else { + name = methodSig; + } + return name; + } + + /** + * Attempt to find corresponding setter method for a given property. + * @return setter method, or null if not found. + */ + private IMethod getSetter(IType type, PropertyInfo propertyInfo) { + try { + String propName = propertyInfo.getName(); + String setterName = "set" + +Character.toUpperCase(propName.charAt(0)) + +toCamelCase(propName.substring(1)); + String sloppySetterName = setterName.toLowerCase(); + + IMethod sloppyMatch = null; + for (IMethod m : type.getMethods().collect(Collectors.toList())) { + String mname = m.getElementName(); + if (setterName.equals(mname)) { + //found 'exact' name match... done + return m; + } else if (mname.toLowerCase().equals(sloppySetterName)) { + sloppyMatch = m; + } + } + return sloppyMatch; + } catch (Exception e) { + log.error("", e); + return null; + } + } + + /** + * Convert hyphened name to camel case name. It is + * safe to call this on an already camel-cased name. + */ + private String toCamelCase(String name) { + if (name.isEmpty()) { + return name; + } else { + StringBuilder camel = new StringBuilder(); + char[] chars = name.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (c=='-') { + i++; + if (i index; - private TypeUtil typeUtil; - private IDocument doc; - private int offset; - private AntlrParser parser; - + + private PropertyFinder propertyFinder; + PropertiesHoverCalculator(FuzzyMap index, TypeUtil typeUtil, IDocument doc, int offset) { - this.index = index; - this.typeUtil = typeUtil; - this.doc = doc; - this.offset = offset; - this.parser = new AntlrParser(); + this.propertyFinder = new PropertyFinder(index, typeUtil, doc, offset); } - + Tuple2 calculate() { - ParseResults parseResults = parser.parse(doc.get()); - Node node = parseResults.ast.findNode(offset); + Node node = propertyFinder.findNode(); if (node instanceof Value) { return getValueHover((Value)node); } else if (node instanceof Key) { @@ -74,35 +60,24 @@ class PropertiesHoverCalculator { } return null; } - - private DocumentRegion createRegion(IDocument doc, Node value) { - // Trim trailing spaces (there is no leading white space already) - int length = value.getLength(); - try { - length = doc.get(value.getOffset(), value.getLength()).length(); - } catch (BadLocationException e) { - // ignore - } - return new DocumentRegion(doc, value.getOffset(), value.getOffset() + length); - } - + private Tuple2 getPropertyHover(Key property) { - PropertyInfo best = findBestHoverMatch(property.decode()); + PropertyInfo best = propertyFinder.findBestHoverMatch(property.decode()); if (best == null) { return null; } else { Renderable renderable = InformationTemplates.createHover(best); - DocumentRegion region = createRegion(doc, property); + DocumentRegion region = propertyFinder.createRegion(property); return Tuples.of(renderable, region.asRegion()); } } - + private Tuple2 getValueHover(Value value) { - DocumentRegion valueRegion = createRegion(doc, value).trimStart(SPACES).trimEnd(SPACES); - if (valueRegion.getStart() <= offset && offset < valueRegion.getEnd()) { + DocumentRegion valueRegion = propertyFinder.createRegion(value).trimStart(SPACES).trimEnd(SPACES); + if (valueRegion.getStart() <= propertyFinder.offset && propertyFinder.offset < valueRegion.getEnd()) { String valueString = valueRegion.toString(); String propertyName = value.getParent().getKey().decode(); - Type type = getValueType(index, typeUtil, propertyName); + Type type = getValueType(propertyFinder.index, propertyFinder.typeUtil, propertyName); if (TypeUtil.isSequencable(type)) { //It is useful to provide content assist for the values in the list when entering a list type = TypeUtil.getDomainType(type); @@ -110,14 +85,14 @@ class PropertiesHoverCalculator { if (TypeUtil.isClass(type)) { //Special case. We want to provide hoverinfos more liberally than what's suggested for completions (i.e. even class names //that are not suggested by the hints because they do not meet subtyping constraints should be hoverable and linkable! - StsValueHint hint = StsValueHint.className(valueString, typeUtil); + StsValueHint hint = StsValueHint.className(valueString, propertyFinder.typeUtil); if (hint!=null) { return Tuples.of(createRenderable(hint), valueRegion.asRegion()); } } //Hack: pretend to invoke content-assist at the end of the value text. This should provide hints applicable to that value // then show hoverinfo based on that. That way we can avoid duplication a lot of similar logic to compute hoverinfos and hyperlinks. - Collection hints = getValueHints(index, typeUtil, valueString, propertyName, EnumCaseMode.ALIASED); + Collection hints = getValueHints(propertyFinder.index, propertyFinder.typeUtil, valueString, propertyName, EnumCaseMode.ALIASED); if (hints!=null) { Optional hint = hints.stream().filter(h -> valueString.equals(h.getValue())).findFirst(); if (hint.isPresent()) { @@ -127,7 +102,7 @@ class PropertiesHoverCalculator { } return null; } - + private Renderable createRenderable(StsValueHint hint) { Renderable description = hint.getDescription(); try { @@ -151,36 +126,4 @@ class PropertiesHoverCalculator { return description; } - /** - * Search known properties for the best 'match' to show as hover data. - */ - private PropertyInfo findBestHoverMatch(String propName) { - PropertyInfo propertyInfo = index.get(propName); - if (propertyInfo == null) { - propertyInfo = SpringPropertyIndex.findLongestValidProperty(index, propName); - } - return propertyInfo; -// //TODO: optimize, should be able to use index's treemap to find this without iterating all entries. -// PropertyInfo best = null; -// int bestCommonPrefixLen = 0; //We try to pick property with longest common prefix -// int bestExtraLen = Integer.MAX_VALUE; -// for (PropertyInfo candidate : index) { -// int commonPrefixLen = StringUtil.commonPrefixLength(propName, candidate.getId()); -// int extraLen = candidate.getId().length()-commonPrefixLen; -// if (commonPrefixLen==propName.length() && extraLen==0) { -// //exact match found, can stop searching for better matches -// return candidate; -// } -// //candidate is better if... -// if (commonPrefixLen>bestCommonPrefixLen // it has a longer common prefix -// || commonPrefixLen==bestCommonPrefixLen && extraLen index; + final TypeUtil typeUtil; + final IDocument doc; + final int offset; + final AntlrParser parser; + + PropertyFinder(FuzzyMap index, TypeUtil typeUtil, IDocument doc, int offset) { + this.index = index; + this.typeUtil = typeUtil; + this.doc = doc; + this.offset = offset; + this.parser = new AntlrParser(); + } + + Node findNode() { + ParseResults parseResults = parser.parse(doc.get()); + return parseResults.ast.findNode(offset); + } + + DocumentRegion createRegion(Node value) { + // Trim trailing spaces (there is no leading white space already) + int length = value.getLength(); + try { + length = doc.get(value.getOffset(), value.getLength()).length(); + } catch (BadLocationException e) { + // ignore + } + return new DocumentRegion(doc, value.getOffset(), value.getOffset() + length); + } + + /** + * Search known properties for the best 'match' to show as hover data. + */ + PropertyInfo findBestHoverMatch(String propName) { + PropertyInfo propertyInfo = index.get(propName); + if (propertyInfo == null) { + propertyInfo = SpringPropertyIndex.findLongestValidProperty(index, propName); + } + return propertyInfo; +// //TODO: optimize, should be able to use index's treemap to find this without iterating all entries. +// PropertyInfo best = null; +// int bestCommonPrefixLen = 0; //We try to pick property with longest common prefix +// int bestExtraLen = Integer.MAX_VALUE; +// for (PropertyInfo candidate : index) { +// int commonPrefixLen = StringUtil.commonPrefixLength(propName, candidate.getId()); +// int extraLen = candidate.getId().length()-commonPrefixLen; +// if (commonPrefixLen==propName.length() && extraLen==0) { +// //exact match found, can stop searching for better matches +// return candidate; +// } +// //candidate is better if... +// if (commonPrefixLen>bestCommonPrefixLen // it has a longer common prefix +// || commonPrefixLen==bestCommonPrefixLen && extraLen url = new VSCodeSourceLinks(null).sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication"); + Optional url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null)).sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication"); assertTrue(url.isPresent()); Path projectPath = Paths.get(project.pom().getParent()); URI uri = URI.create(url.get()); - + // Use File to get rid of the fragment parts of the URL. The URL may have fragments that indicate line and column numbers uri = new File(uri.getPath()).toURI(); @@ -72,7 +73,7 @@ public class VSCodeSourceLinksTest { @Test public void testJarUrl() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = new VSCodeSourceLinks(null).sourceLinkUrlForFQName(project, "org.springframework.boot.autoconfigure.SpringBootApplication"); + Optional url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null)).sourceLinkUrlForFQName(project, "org.springframework.boot.autoconfigure.SpringBootApplication"); assertTrue(url.isPresent()); String headerPart = url.get().substring(0, url.get().indexOf('?')); assertEquals("jdt://contents/spring-boot-autoconfigure-1.5.8.RELEASE.jar/org.springframework.boot.autoconfigure/SpringBootApplication.class", headerPart); @@ -83,7 +84,7 @@ public class VSCodeSourceLinksTest { @Test public void testJarUrlInnerType() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = new VSCodeSourceLinks(null).sourceLinkUrlForFQName(project, "org.springframework.web.client.RestTemplate$AcceptHeaderRequestCallback"); + Optional url = new VSCodeSourceLinks(new CompilationUnitCache(null, null, null)).sourceLinkUrlForFQName(project, "org.springframework.web.client.RestTemplate$AcceptHeaderRequestCallback"); assertTrue(url.isPresent()); String headerPart = url.get().substring(0, url.get().indexOf('?')); assertEquals("jdt://contents/spring-web-4.3.12.RELEASE.jar/org.springframework.web.client/RestTemplate$AcceptHeaderRequestCallback.class", headerPart); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java index 22fd6f9c0..7e7886a80 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java @@ -29,11 +29,15 @@ import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; +import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.languageserver.testharness.Editor; @@ -90,6 +94,11 @@ public class ValueCompletionTest { null ); } + + @Bean SourceLinks sourceLinks(SimpleTextDocumentService documents, CompilationUnitCache cuCache) { + return SourceLinkFactory.NO_SOURCE_LINKS; + } + } @Before diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java index 537176254..a72f4d2d3 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java @@ -16,14 +16,30 @@ import static org.springframework.ide.vscode.boot.properties.reconcile.Applicati import static org.springframework.ide.vscode.languageserver.testharness.ClasspathTestUtil.getOutputFolder; import static org.springframework.ide.vscode.languageserver.testharness.TestAsserts.assertContains; +import java.net.URI; +import java.net.URL; import java.nio.charset.Charset; import java.nio.file.Path; import java.time.Duration; +import java.util.Arrays; import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.EnumConstantDeclaration; +import org.eclipse.jdt.core.dom.EnumDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.Diagnostic; -import org.junit.Before; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Range; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,22 +47,28 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.ide.vscode.boot.app.BootLanguageServerInitializer; import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; import org.springframework.ide.vscode.boot.bootiful.PropertyEditorTestConf; import org.springframework.ide.vscode.boot.editor.harness.AbstractPropsEditorTest; import org.springframework.ide.vscode.boot.editor.harness.StyledStringMatcher; +import org.springframework.ide.vscode.boot.java.links.JavaDocumentUriProvider; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.metadata.CachingValueProvider; import org.springframework.ide.vscode.boot.metadata.PropertiesLoader; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.IType; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; +import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.project.harness.ProjectsHarness.ProjectCustomizer; import org.springframework.test.context.junit4.SpringRunner; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.io.Files; /** @@ -59,6 +81,15 @@ import com.google.common.io.Files; @Import(PropertyEditorTestConf.class) public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { + @Autowired + CompilationUnitCache cuCache; + + @Autowired + SimpleTextDocumentService docService; + + @Autowired + JavaDocumentUriProvider javaDocumentUriProvider; + @Configuration static class TestConf { @Bean LanguageId defaultLanguageId() { return LanguageId.BOOT_PROPERTIES; @@ -275,7 +306,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { assertContains("\"name\": \"foo.counter\"", Files.toString(metadataFile.toFile(), Charset.forName("UTF8"))); } - @Ignore @Test public void testHyperlinkTargets() throws Exception { + @Test public void testHyperlinkTargets() throws Exception { System.out.println(">>> testHyperlinkTargets"); IJavaProject p = createPredefinedMavenProject("tricky-getters-boot-1.3.1-app"); useProject(p); @@ -286,20 +317,260 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { "flyway.init-sqls=a,b,c\n" ); - editor.assertLinkTargets("server", - "org.springframework.boot.autoconfigure.web.ServerProperties.setPort(Integer)" + assertLinkTargets(editor, "server", p, + method("org.springframework.boot.autoconfigure.web.ServerProperties", "setPort", "java.lang.Integer")); + + assertLinkTargets(editor, "data", p, + method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "hikariDataSource"), + method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "tomcatDataSource"), + method("org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata", "dbcpDataSource") ); - editor.assertLinkTargets("data", - "org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata.hikariDataSource()", - "org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata.tomcatDataSource()", - "org.springframework.boot.autoconfigure.jdbc.DataSourceConfigMetadata.dbcpDataSource()" - ); - editor.assertLinkTargets("flyway", - "org.springframework.boot.autoconfigure.flyway.FlywayProperties.setInitSqls(List)"); + + assertLinkTargets(editor, "flyway", p, method("org.springframework.boot.autoconfigure.flyway.FlywayProperties", "setInitSqls", "java.util.List")); System.out.println("<<< testHyperlinkTargets"); } - @Ignore @Test public void testHyperlinkTargetsLoggingLevel() throws Exception { + void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, JavaMethod... methods) throws Exception { + Set expectedLocations = Arrays.stream(methods).map(method -> { + try { + return getLocation(project, method); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }).collect(Collectors.toSet()); + + editor.assertLinkTargets(hoverOver, expectedLocations); + } + + void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, String typeFqName) throws Exception { + + Location expectedLocation = getLocation(project, typeFqName); + + System.out.println("Expected Location: " + expectedLocation); + + editor.assertLinkTargets(hoverOver, ImmutableSet.of(expectedLocation)); + } + + void assertLinkTargets(Editor editor, String hoverOver, IJavaProject project, JavaField field) throws Exception { + + Location expectedLocation = getLocation(project, field); + + System.out.println("Expected Location: " + expectedLocation); + + editor.assertLinkTargets(hoverOver, ImmutableSet.of(expectedLocation)); + } + + static private JavaMethod method(String fqClassName, String methodName, String... params) { + return new JavaMethod(fqClassName, methodName, params); + } + + static private JavaField field(String fqClassName, String name) { + return new JavaField(fqClassName, name); + } + + static class JavaMethod { + public final String fqName; + public final String methodName; + public final String[] params; + public JavaMethod(String fqClassName, String methodName, String... params) { + this.fqName = fqClassName; + this.methodName = methodName; + this.params = params; + } + @Override + public String toString() { + return "JavaMethod [fqName=" + fqName + ", methodName=" + methodName + ", params=" + Arrays.toString(params) + + "]"; + } + } + + static class JavaField { + public final String fqName; + public final String fieldName; + public JavaField(String fqName, String fieldName) { + super(); + this.fqName = fqName; + this.fieldName = fieldName; + } + @Override + public String toString() { + return "JavaField [fqName=" + fqName + ", fieldName=" + fieldName + "]"; + } + } + + private Location getLocation(IJavaProject project, String fqName) throws Exception { + Location loc = new Location(); + Optional sourceUrl = SourceLinks.source(project, fqName); + if (sourceUrl.isPresent()) { + + URI docUri = javaDocumentUriProvider.docUri(project, fqName); + loc.setUri(docUri.toString()); + + String typeName = fqName.substring(fqName.lastIndexOf('.') + 1); + URI sourceUri = sourceUrl.get().toURI(); + Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { + try { + TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); + doc.setText(cuCache.fetchContent(sourceUri)); + AtomicReference range = new AtomicReference<>(null); + cu.accept(new ASTVisitor() { + + private boolean proceessTypeNode(TextDocument doc, String typeName, + AtomicReference range, AbstractTypeDeclaration node) { + SimpleName nameNode = node.getName(); + if (nameNode.getIdentifier().equals(typeName)) { + try { + range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); + return false; + } catch (BadLocationException e) { + throw new IllegalStateException(e); + } + } + return true; + } + + @Override + public boolean visit(TypeDeclaration node) { + return proceessTypeNode(doc, typeName, range, node); + } + + @Override + public boolean visit(EnumDeclaration node) { + return proceessTypeNode(doc, typeName, range, node); + } + + }); + return range.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + if (r == null) { + throw new IllegalStateException("Couldn't find " + fqName); + } + loc.setRange(r); + } + return loc; + } + + private Location getLocation(IJavaProject project, JavaMethod method) throws Exception { + Location loc = new Location(); + Optional sourceUrl = SourceLinks.source(project, method.fqName); + if (sourceUrl.isPresent()) { + + URI docUri = javaDocumentUriProvider.docUri(project, method.fqName); + loc.setUri(docUri.toString()); + + URI sourceUri = sourceUrl.get().toURI(); + Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { + try { + AtomicReference range = new AtomicReference<>(null); + TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); + doc.setText(cuCache.fetchContent(sourceUri)); + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(MethodDeclaration node) { + SimpleName nameNode = node.getName(); + if (nameNode.getIdentifier().equals(method.methodName)) { + if (node.parameters().size() != method.params.length) { + return false; + } + int i = 0; + for (Object _p : node.parameters()) { + if (_p instanceof SingleVariableDeclaration) { + SingleVariableDeclaration p = (SingleVariableDeclaration) _p; + String fqName = p.getType().resolveBinding().getErasure().getQualifiedName(); + if (!fqName.equals(method.params[i++])) { + return false; + } + } else { + return false; + } + } + try { + range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); + } catch (BadLocationException e) { + throw new IllegalStateException(e); + } + } + return false; + } + + }); + return range.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + if (r == null) { + throw new IllegalStateException("Couldn't find " + method); + } + loc.setRange(r); + } + return loc; + } + + private Location getLocation(IJavaProject project, JavaField field) throws Exception { + Location loc = new Location(); + + Optional sourceUrl = SourceLinks.source(project, field.fqName); + if (sourceUrl.isPresent()) { + + URI sourceUri = sourceUrl.get().toURI(); + + URI docUri = javaDocumentUriProvider.docUri(project, field.fqName); + loc.setUri(docUri.toString()); + + Range r = cuCache.withCompilationUnit(project, sourceUri, (cu) -> { + try { + AtomicReference range = new AtomicReference<>(null); + TextDocument doc = new TextDocument(sourceUrl.get().toString(), LanguageId.JAVA); + doc.setText(cuCache.fetchContent(sourceUri)); + cu.accept(new ASTVisitor() { + + boolean foundType = false; + + @Override + public boolean visit(EnumConstantDeclaration node) { + if (foundType) { + SimpleName nameNode = node.getName(); + if (nameNode.getIdentifier().equals(field.fieldName)) { + try { + range.set(doc.toRange(nameNode.getStartPosition(), nameNode.getLength())); + } catch (BadLocationException e) { + throw new IllegalStateException(e); + } + } + } + return true; + } + + @Override + public boolean visit(EnumDeclaration node) { + if (node.getName().getIdentifier() + .equals(field.fqName.substring(field.fqName.lastIndexOf('.') + 1))) { + foundType = true; + return true; + } + return false; + } + + }); + return range.get(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + if (r == null) { + throw new IllegalStateException("Couldn't find " + field); + } + loc.setRange(r); + } + return loc; + } + + @Test public void testHyperlinkTargetsLoggingLevel() throws Exception { System.out.println(">>> testHyperlinkTargetsLoggingLevel"); IJavaProject p = createPredefinedMavenProject("tricky-getters-boot-1.3.1-app"); @@ -308,9 +579,8 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { Editor editor = newEditor( "logging.level.com.acme=INFO\n" ); - editor.assertLinkTargets("level", - "org.springframework.boot.logging.LoggingApplicationListener" - ); + + assertLinkTargets(editor, "level", p, "org.springframework.boot.logging.LoggingApplicationListener"); System.out.println("<<< testHyperlinkTargetsLoggingLevel"); } @@ -1422,16 +1692,17 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { ); } - @Ignore @Test public void testClassReferenceInValueLink() throws Exception { + @Test public void testClassReferenceInValueLink() throws Exception { Editor editor; - useProject(createPredefinedMavenProject("empty-boot-1.3.0-with-mongo")); + MavenJavaProject project = createPredefinedMavenProject("empty-boot-1.3.0-with-mongo"); + useProject(project); editor = newEditor( "#stuff\n" + "spring.data.mongodb.field-naming-strategy=org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy\n" + "#more stuff" ); - editor.assertLinkTargets("org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); + assertLinkTargets(editor, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy", project, "org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy"); //Linking should also work for types that aren't valid based on the constraints editor = newEditor( @@ -1439,7 +1710,7 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { "spring.data.mongodb.field-naming-strategy=java.lang.String\n" + "#more stuff" ); - editor.assertLinkTargets("java.lang.String", "java.lang.String"); + assertLinkTargets(editor, "java.lang.String", project, "java.lang.String"); } @Test public void testCommaListReconcile() throws Exception { @@ -1601,9 +1872,9 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { editor.assertHoverContains("red", "Hot and delicious"); } - - @Ignore @Test public void testEnumInValueLink() throws Exception { - useProject(createPredefinedMavenProject("enums-boot-1.3.2-app")); + @Test public void testEnumInValueLink() throws Exception { + MavenJavaProject project = createPredefinedMavenProject("enums-boot-1.3.2-app"); + useProject(project); data("my.background", "demo.Color", null, "Color to use as default background."); Editor editor; @@ -1611,12 +1882,25 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { editor = newEditor( "my.background: RED" ); - editor.assertLinkTargets("RED", "demo.Color.RED"); + assertLinkTargets(editor, "RED", project, field("demo.Color", "RED")); editor = newEditor( "my.background=red" ); - editor.assertLinkTargets("red", "demo.Color.RED"); + assertLinkTargets(editor, "red", project, field("demo.Color", "RED")); + } + + @Test public void testEnumInPojoField() throws Exception { + MavenJavaProject project = createPredefinedMavenProject("enum-def-nav"); + useProject(project); + + Editor editor; + + editor = newEditor( + "my.screen.background=green" + ); + assertLinkTargets(editor, "background", project, method("com.example.demo.MyProperties$Screen", "getScreen")); + assertLinkTargets(editor, "green", project, field("com.example.demo.Color", "GREEN")); } @Test public void testNoHoverForUnrecognizedProperty() throws Exception { diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/.mvn/wrapper/maven-wrapper.jar b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..01e679973 Binary files /dev/null and b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/.mvn/wrapper/maven-wrapper.jar differ diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/.mvn/wrapper/maven-wrapper.properties b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..717934671 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/mvnw b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/mvnw new file mode 100755 index 000000000..5551fde8e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/mvnw.cmd b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/mvnw.cmd new file mode 100644 index 000000000..e5cfb0ae9 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/pom.xml new file mode 100644 index 000000000..51f900ed3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + com.example + enum-def-nav + 0.0.1-SNAPSHOT + jar + + enum-def-nav + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.1.0.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/Color.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/Color.java new file mode 100644 index 000000000..8bfa72d60 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/Color.java @@ -0,0 +1,7 @@ +package com.example.demo; + +public enum Color { + RED, + GREEN, + BLUE +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/EnumDefNavApplication.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/EnumDefNavApplication.java new file mode 100644 index 000000000..22f0f7d7b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/EnumDefNavApplication.java @@ -0,0 +1,12 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EnumDefNavApplication { + + public static void main(String[] args) { + SpringApplication.run(EnumDefNavApplication.class, args); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/MyProperties.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/MyProperties.java new file mode 100644 index 000000000..b531d6683 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/main/java/com/example/demo/MyProperties.java @@ -0,0 +1,65 @@ +package com.example.demo; + +import java.util.List; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("my") +public class MyProperties { + + public static class Screen { + private Color foreground; + private Color background; + public Color getForeground() { + return foreground; + } + public void setForeground(Color foreground) { + this.foreground = foreground; + } + public Color getBackground() { + return background; + } + public void setBackground(Color background) { + this.background = background; + } + } + + private Screen screen; + private Screen[] screenArray; + private List screenList; + private Map screenMap; + + public Screen getScreen() { + return screen; + } + + public void setScreen(Screen screen) { + this.screen = screen; + } + + public Screen[] getScreenArray() { + return screenArray; + } + + public void setScreenArray(Screen[] screenArray) { + this.screenArray = screenArray; + } + + public List getScreenList() { + return screenList; + } + + public void setScreenList(List screenList) { + this.screenList = screenList; + } + + public Map getScreenMap() { + return screenMap; + } + + public void setScreenMap(Map screenMap) { + this.screenMap = screenMap; + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/test/java/com/example/demo/EnumDefNavApplicationTests.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/test/java/com/example/demo/EnumDefNavApplicationTests.java new file mode 100644 index 000000000..f5b8fbd63 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/enum-def-nav/src/test/java/com/example/demo/EnumDefNavApplicationTests.java @@ -0,0 +1,16 @@ +package com.example.demo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class EnumDefNavApplicationTests { + + @Test + public void contextLoads() { + } + +}