diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java index 9445ba2c4..c8eb925d5 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java @@ -269,9 +269,9 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { providers.put(Annotations.SPRING_PATCH_MAPPING, new RequestMappingHoverProvider()); providers.put(Annotations.PROFILE, new ActiveProfilesProvider()); - providers.put(Annotations.AUTOWIRED, new AutowiredHoverProvider()); - providers.put(Annotations.COMPONENT, new ComponentInjectionsHoverProvider()); - providers.put(Annotations.BEAN, new BeanInjectedIntoHoverProvider()); + providers.put(Annotations.AUTOWIRED, new AutowiredHoverProvider(this)); + providers.put(Annotations.COMPONENT, new ComponentInjectionsHoverProvider(this)); + providers.put(Annotations.BEAN, new BeanInjectedIntoHoverProvider(this)); providers.put(Annotations.CONDITIONAL, new ConditionalsLiveHoverProvider()); providers.put(Annotations.CONDITIONAL_ON_BEAN, new ConditionalsLiveHoverProvider()); diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index c20afae05..7bd91c20c 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -22,6 +22,7 @@ import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; 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.livehover.ComponentInjectionsHoverProvider; @@ -41,6 +42,12 @@ import com.google.common.collect.ImmutableList; * @author Martin Lippert */ public class AutowiredHoverProvider implements HoverProvider { + + private BootJavaLanguageServer server; + + public AutowiredHoverProvider(BootJavaLanguageServer server) { + this.server = server; + } @Override public Collection getLiveHoverHints(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { @@ -155,7 +162,7 @@ public class AutowiredHoverProvider implements HoverProvider { } List dependencyBeans = beans.getBeansOfName(injectedBean); for (LiveBean dependencyBean : dependencyBeans) { - hover.append("- " + LiveHoverUtils.showBeanWithResource(dependencyBean, " ", project)); + hover.append("- " + LiveHoverUtils.showBeanWithResource(server, dependencyBean, " ", project)); } firstDependency = false; } diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java new file mode 100644 index 000000000..081098827 --- /dev/null +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/AbstractSourceLinks.java @@ -0,0 +1,253 @@ +/******************************************************************************* + * 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.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +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.TypeDeclaration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.javadoc.SourceUrlProviderFromSourceContainer; +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 + * + * @author Alex Boyko + * + */ +public abstract class AbstractSourceLinks implements SourceLinks { + + private static Supplier LOG = Suppliers.memoize(() -> LoggerFactory.getLogger(AbstractSourceLinks.class)); + + private BootJavaLanguageServer server; + + protected AbstractSourceLinks(BootJavaLanguageServer server) { + this.server = server; + } + + @Override + public Optional sourceLinkUrlForFQName(IJavaProject project, String fqName) { + Optional classpathResource = project.getClasspath().findClasspathResourceContainer(fqName); + if (classpathResource.isPresent()) { + File file = classpathResource.get(); + if (file.isDirectory()) { + return javaSourceLinkUrl(project, fqName, file); + } else if (file.getName().endsWith(JAR)) { + return jarSourceLinkUrl(project, fqName, file); + } + } + return Optional.empty(); + } + + @Override + public Optional sourceLinkUrlForClasspathResource(IJavaProject project, String path) { + int idx = path.lastIndexOf(CLASS); + if (idx >= 0) { + Path p = Paths.get(path.substring(0, idx)); + return sourceLinkUrlForFQName(project, p.toString().replace(File.separator, ".")); + } + return Optional.empty(); + } + + private Optional javaSourceLinkUrl(IJavaProject project, String fqName, File containerFolder) { + IClasspath classpath = project.getClasspath(); + if (containerFolder.toPath().startsWith(classpath.getOutputFolder())) { + return project.getClasspath().getSourceFolders().stream() + .map(sourceFolder -> { + try { + return Paths.get(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 SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER.sourceUrl(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() + .map(sourcePath -> javaSourceLinkUrl(project, sourcePath, fqName)); + } + return Optional.empty(); + } + + 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)); + 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 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); + + 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 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.getClasspath().sourceContainer(jarFile) + .map(url -> { + try { + return SourceUrlProviderFromSourceContainer.JAR_SOURCE_URL_PROVIDER.sourceUrl(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; + } + + protected Region findTypeRegion(CompilationUnit cu, String fqName) { + if (cu == null) { + return null; + } + int[] values = new int[] {0, -1}; + int lastDotIndex = fqName.lastIndexOf('.'); + String packageName = fqName.substring(0, lastDotIndex); + String typeName = fqName.substring(lastDotIndex + 1); + 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) { + if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) { + values[0] = node.getName().getStartPosition(); + values[1] = node.getName().getLength(); + } + } + return values[1] < 0; + } + + @Override + public boolean visit(TypeDeclaration node) { + return visitDeclaration(node); + } + + @Override + public boolean visit(AnnotationTypeDeclaration node) { + return visitDeclaration(node); + } + + @Override + public void endVisit(AnnotationTypeDeclaration node) { + visitedType.pop(); + super.endVisit(node); + } + + @Override + public void endVisit(TypeDeclaration node) { + visitedType.pop(); + super.endVisit(node); + } + + }); + } + return values[1] < 0 ? null : new Region(values[0], values[1]); + } + +} diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java new file mode 100644 index 000000000..a9880cb19 --- /dev/null +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinkFactory.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * 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.nio.file.Path; +import java.util.Optional; + +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; +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() { + + @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(BootJavaLanguageServer server) { + switch (LspClient.currentClient()) { + case VSCODE: + return new VSCodeSourceLinks(server); + default: + return NO_SOURCE_LINKS; + } + + } + +} diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java new file mode 100644 index 000000000..43418fb8d --- /dev/null +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/SourceLinks.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * 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.nio.file.Path; +import java.util.Optional; + +import org.springframework.ide.vscode.commons.java.IJavaProject; + +/** + * 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 String JAR = ".jar"; + static final String CLASS = ".class"; + + /** + * 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 + * @param fqName type's fully qualified name + * @return the link URL optional + */ + Optional sourceLinkUrlForFQName(IJavaProject project, String fqName); + + /** + * Creates link to source file corresponding to a classpath resource + * @param project project Java project in the context of which source file link is calculated + * @param path the path to the classpath resource + * @return the link URL optional + */ + Optional sourceLinkUrlForClasspathResource(IJavaProject project, String 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/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java new file mode 100644 index 000000000..5249c4834 --- /dev/null +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/links/VSCodeSourceLinks.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * 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.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.BootJavaLanguageServer; +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(BootJavaLanguageServer server) { + super(server); + } + + @Override + public Optional sourceLinkForResourcePath(Path path) { + return Optional.of(path.toUri().toString()); + } + + @Override + protected String positionLink(CompilationUnit cu, String fqName) { + if (cu != null) { + Region region = findTypeRegion(cu, fqName); + if (region != null) { + int column = cu.getColumnNumber(region.getOffset()); + int line = cu.getLineNumber(region.getOffset()); + StringBuilder sb = new StringBuilder(); + sb.append('#'); + sb.append(line); + sb.append(','); + sb.append(column + 1); // 1-based columns? + return sb.toString(); + } + } + return null; + } + + @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(); + } + +} diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java index ab4c2673a..de4f9f914 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal, Inc. + * Copyright (c) 2017, 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 @@ -22,6 +22,7 @@ import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; @@ -34,6 +35,12 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider { + + protected BootJavaLanguageServer server; + + public AbstractInjectedIntoHoverProvider(BootJavaLanguageServer server) { + this.server = server; + } @Override public Collection getLiveHoverHints(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { @@ -113,7 +120,7 @@ public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider if (!firstDependency) { hover.append("\n"); } - hover.append("- " + LiveHoverUtils.showBeanWithResource(dependingBean, " ", project)); + hover.append("- " + LiveHoverUtils.showBeanWithResource(server, dependingBean, " ", project)); firstDependency = false; } } diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java index e4e18e923..c6389ed57 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; 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; @@ -29,6 +30,10 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; public class BeanInjectedIntoHoverProvider extends AbstractInjectedIntoHoverProvider { + public BeanInjectedIntoHoverProvider(BootJavaLanguageServer server) { + super(server); + } + @Override protected LiveBean getDefinedBean(Annotation annotation) { MethodDeclaration beanMethod = ASTUtils.getAnnotatedMethod(annotation); diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java index 8c3d2c507..ddb26300d 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java @@ -26,6 +26,7 @@ import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.FunctionUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; @@ -43,6 +44,10 @@ import reactor.util.function.Tuple3; public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverProvider { + public ComponentInjectionsHoverProvider(BootJavaLanguageServer server) { + super(server); + } + @Override protected void addAutomaticallyWiredContructor(StringBuilder hover, Annotation annotation, LiveBeansModel beans, LiveBean bean, IJavaProject project) { TypeDeclaration typeDecl = ASTUtils.findDeclaringType(annotation); @@ -63,7 +68,7 @@ public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverP } List dependencyBeans = beans.getBeansOfName(injectedBean); for (LiveBean dependencyBean : dependencyBeans) { - hover.append("- " + LiveHoverUtils.showBeanWithResource(dependencyBean, " ", project)); + hover.append("- " + LiveHoverUtils.showBeanWithResource(server, dependencyBean, " ", project)); } firstDependency = false; } diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java index d74be1a68..84624cb2f 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java @@ -13,7 +13,9 @@ package org.springframework.ide.vscode.boot.java.livehover; import java.util.Optional; import java.util.stream.Stream; -import org.springframework.ide.vscode.boot.java.utils.SourceLinks; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; +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; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; @@ -23,7 +25,7 @@ import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.StringUtil; public class LiveHoverUtils { - + public static String showBean(LiveBean bean) { StringBuilder buf = new StringBuilder("Bean [id: " + bean.getId()); String type = bean.getType(true); @@ -34,18 +36,19 @@ public class LiveHoverUtils { return buf.toString(); } - public static String showBeanWithResource(LiveBean bean, String indentStr, IJavaProject project) { + public static String showBeanWithResource(BootJavaLanguageServer server, 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 String type = bean.getType(true); StringBuilder buf = new StringBuilder("Bean: "); buf.append(bean.getId()); + SourceLinks sourceLinks = SourceLinkFactory.createSourceLinks(server); if (type != null) { // Try creating a URL link to open source for the type buf.append(newline); buf.append("Type: "); - Optional url = SourceLinks.sourceLinkUrlForFQName(project, type); + Optional url = sourceLinks.sourceLinkUrlForFQName(project, type); if (url.isPresent()) { buf.append(Renderables.link(type, url.get()).toMarkdown()); } else { @@ -56,13 +59,13 @@ public class LiveHoverUtils { if (StringUtil.hasText(resource)) { buf.append(newline); buf.append("Resource: "); - buf.append(showResource(resource, project)); + buf.append(showResource(sourceLinks, resource, project)); } return buf.toString(); } - public static String showResource(String resource, IJavaProject project) { - return new SpringResource(resource, project).toMarkdown(); + public static String showResource(SourceLinks sourceLinks, String resource, IJavaProject project) { + return new SpringResource(sourceLinks, resource, project).toMarkdown(); } public static String niceAppName(SpringBootApp app) { diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java index e311853ef..0e1a9abf6 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -133,6 +133,14 @@ public final class CompilationUnitCache { } public static CompilationUnit parse(TextDocument document, IJavaProject project) throws Exception { + String[] classpathEntries = getClasspathEntries(document, 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(char[] source, String docURI, String unitName, String[] classpathEntries) throws Exception { ASTParser parser = ASTParser.newParser(AST.JLS9); Map options = JavaCore.getOptions(); JavaCore.setComplianceOptions(JavaCore.VERSION_1_8, options); @@ -142,14 +150,11 @@ public final class CompilationUnitCache { parser.setBindingsRecovery(true); parser.setResolveBindings(true); - String[] classpathEntries = getClasspathEntries(document, project); String[] sourceEntries = new String[] {}; parser.setEnvironment(classpathEntries, sourceEntries, null, true); - String docURI = document.getUri(); - String unitName = docURI.substring(docURI.lastIndexOf("/")); parser.setUnitName(unitName); - parser.setSource(document.get(0, document.getLength()).toCharArray()); + parser.setSource(source); CompilationUnit cu = (CompilationUnit) parser.createAST(null); diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/SourceLinks.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/SourceLinks.java deleted file mode 100644 index 1cdf28967..000000000 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/SourceLinks.java +++ /dev/null @@ -1,180 +0,0 @@ -/******************************************************************************* - * 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.io.File; -import java.net.URLEncoder; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Optional; - -import org.springframework.ide.vscode.commons.java.IClasspath; -import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient; -import org.springframework.ide.vscode.commons.util.Log; - -/** - * Utility for creating code navigate links for Markdown format documentation. - * The utility looks at the environment variable "sts.lsp.client" to create a URL supported on a specific client - * - * @author Alex Boyko - * - */ -public class SourceLinks { - - static final String JAR = ".jar"; - static final String JAVA = ".java"; - static final String CLASS = ".class"; - - public static Optional sourceLinkUrlForFQName(IJavaProject project, String fqName) { - switch (LspClient.currentClient()) { - case VSCODE: - return createVSCodeSourceLink(project, fqName); - default: - return Optional.empty(); - } - } - - public static Optional sourceLinkUrlForClasspathResource(IJavaProject project, String path) { - switch (LspClient.currentClient()) { - case VSCODE: - return createVSCodeSourceLinkForClasspathResource(project, path); - default: - return Optional.empty(); - } - } - - public static Optional sourceLinkForResourcePath(Path path) { - switch (LspClient.currentClient()) { - case VSCODE: - return Optional.of(createVSCodeLink(path)); - default: - return Optional.empty(); - } - } - - private static Optional createVSCodeSourceLinkForClasspathResource(IJavaProject project, String path) { - int idx = path.lastIndexOf(SourceLinks.CLASS); - if (idx >= 0) { - Path p = Paths.get(path.substring(0, idx)); - return SourceLinks.sourceLinkUrlForFQName(project, p.toString().replace(File.separator, ".")); - } - return Optional.empty(); - } - - private static String createVSCodeLink(Path path) { - return path.toUri().toString(); - } - - private static Optional createVSCodeSourceLink(IJavaProject project, String fqName) { - Optional classpathResource = project.getClasspath().findClasspathResourceContainer(fqName); - if (classpathResource.isPresent()) { - File file = classpathResource.get(); - if (file.isDirectory()) { - return javaSourceLinkUrl(project, fqName, file); - } else if (file.getName().endsWith(JAR)) { - return jarSourceLinkUrl(project, fqName, file); - } - } - return Optional.empty(); - } - - private static Optional javaSourceLinkUrl(IJavaProject project, String javaRelativePath, String fqName) { - Path path = Paths.get(javaRelativePath); - Optional sourceResource = project.getClasspath().getSourceFolders().stream() - .map(r -> Paths.get(r).resolve(path)).filter(r -> Files.exists(r)).findFirst(); - if (sourceResource.isPresent()) { - return Optional.of(sourceResource.get().toUri().toString()); - } - return Optional.empty(); - } - - private static Optional javaSourceLinkUrl(IJavaProject project, String fqName, File containerFolder) { - IClasspath classpath = project.getClasspath(); - if (containerFolder.toPath().startsWith(classpath.getOutputFolder())) { - int innerTypeIdx = fqName.indexOf('$'); - String topLevelType = innerTypeIdx > 0 ? fqName.substring(0, innerTypeIdx) : fqName; - return javaSourceLinkUrl(project, topLevelType.replaceAll("\\.", "/") + JAVA, fqName); - } - return Optional.empty(); - } - - private static Optional jarSourceLinkUrl(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.log(t); - } - return Optional.empty(); - } - -// private static Optional findTypeRange(CompilationUnit cu, String fqName) { -// int[] values = new int[] {0, -1}; -// int lastDotIndex = fqName.lastIndexOf('.'); -// String packageName = fqName.substring(0, lastDotIndex); -// String typeName = fqName.substring(lastDotIndex + 1); -// if (packageName.equals(cu.getPackage().getName().getFullyQualifiedName())) { -// Stack visitedType = new Stack<>(); -// cu.accept(new ASTVisitor() { -// -// @Override -// public boolean visit(TypeDeclaration node) { -// if (values[1] < 0) { -// visitedType.push(node.getName().getIdentifier()); -// if (String.join("$", visitedType.toArray(new String[visitedType.size()])).equals(typeName)) { -// values[0] = node.getName().getStartPosition(); -// values[1] = node.getName().getLength(); -// } -// } -// return values[1] < 0; -// } -// -// @Override -// public void endVisit(TypeDeclaration node) { -// visitedType.pop(); -// super.endVisit(node); -// } -// -// }); -// } -// return Optional.ofNullable(values[1] < 0 ? null : new Region(values[0], values[1])); -// } - -} diff --git a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java index 11990ec24..a518c4f6e 100644 --- a/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java +++ b/headless-services/commons/commons-boot/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java @@ -10,16 +10,15 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.utils; -import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; /** @@ -31,13 +30,15 @@ public class SpringResource { private static final String FILE = "file"; private static final String CLASS_PATH_RESOURCE = "class path resource"; + private SourceLinks sourceLinks; private String type; private String path; private IJavaProject project; private static final Pattern BRACKETS = Pattern.compile("\\[[^\\]]*\\]"); - public SpringResource(String toParse, IJavaProject project) { + public SpringResource(SourceLinks sourceLinks, String toParse, IJavaProject project) { + this.sourceLinks = sourceLinks; this.project = project; Matcher matcher = BRACKETS.matcher(toParse); if (matcher.find()) { @@ -57,15 +58,15 @@ public class SpringResource { case FILE: String relativePath = projectRelativePath(path); if (relativePath != path && path.endsWith(SourceLinks.CLASS)) { - linkUrl = SourceLinks.sourceLinkUrlForClasspathResource(project, relativePath); + linkUrl = sourceLinks.sourceLinkUrlForClasspathResource(project, relativePath); } else { - linkUrl = SourceLinks.sourceLinkForResourcePath(Paths.get(path)); + linkUrl = sourceLinks.sourceLinkForResourcePath(Paths.get(path)); } // not a project relative path return linkUrl.isPresent() ? Renderables.link(relativePath, linkUrl.get()).toMarkdown() : "`" + projectRelativePath(path) + "`"; case CLASS_PATH_RESOURCE: - linkUrl = SourceLinks.sourceLinkUrlForClasspathResource(project, path); + linkUrl = sourceLinks.sourceLinkUrlForClasspathResource(project, path); return linkUrl.isPresent() ? Renderables.link(path, linkUrl.get()).toMarkdown() : "`"+path+"`"; default: return path; diff --git a/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java b/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java index 7e776b422..e6195d8b9 100644 --- a/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java +++ b/headless-services/commons/commons-boot/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/VSCodeSourceLinksTest.java @@ -21,7 +21,7 @@ import java.util.Optional; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.springframework.ide.vscode.boot.java.utils.SourceLinks; +import org.springframework.ide.vscode.boot.java.links.VSCodeSourceLinks; import org.springframework.ide.vscode.commons.languageserver.util.LspClient; import org.springframework.ide.vscode.commons.maven.MavenBuilder; import org.springframework.ide.vscode.commons.maven.MavenCore; @@ -63,29 +63,35 @@ public class VSCodeSourceLinksTest { @Test public void testJavaSourceUrl() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = SourceLinks.sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication"); + Optional url = new VSCodeSourceLinks(null).sourceLinkUrlForFQName(project, "com.example.EmptyBoot15WebAppApplication"); assertTrue(url.isPresent()); Path projectPath = Paths.get(project.pom().getParent()); - Path relativePath = projectPath.relativize(Paths.get(new URL(url.get()).toURI())); + Path relativePath = projectPath.relativize(Paths.get(new URL(url.get()).getPath())); assertEquals(Paths.get("src/main/java/com/example/EmptyBoot15WebAppApplication.java"), relativePath); + String positionPart = url.get().substring(url.get().lastIndexOf('#')); + assertEquals("#7,14", positionPart); } @Test public void testJarUrl() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = SourceLinks.sourceLinkUrlForFQName(project, "org.springframework.boot.autoconfigure.SpringBootApplication"); + Optional url = new VSCodeSourceLinks(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); + String positionPart = url.get().substring(url.get().lastIndexOf('#')); + assertEquals("#55,19", positionPart); } @Test public void testJarUrlInnerType() throws Exception { MavenJavaProject project = mavenProjectsCache.get("empty-boot-15-web-app"); - Optional url = SourceLinks.sourceLinkUrlForFQName(project, "org.springframework.web.client.RestTemplate$AcceptHeaderRequestCallback"); + Optional url = new VSCodeSourceLinks(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); + String positionPart = url.get().substring(url.get().lastIndexOf('#')); + assertEquals("#747,16", positionPart); } } diff --git a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectClasspath.java b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectClasspath.java index 3559c42a8..fc036eb61 100644 --- a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectClasspath.java +++ b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectClasspath.java @@ -68,7 +68,7 @@ public class GradleProjectClasspath extends JandexClasspath { URL javadocUrl = new URL("https://docs.oracle.com/javase/" + javaVersion + "/docs/api/"); return new HtmlJavadocProvider( (type) -> SourceUrlProviderFromSourceContainer.JAVADOC_FOLDER_URL_SUPPLIER - .sourceUrl(javadocUrl, type)); + .sourceUrl(javadocUrl, type.getFullyQualifiedName())); } catch (MalformedURLException e) { Log.log(e); return null; @@ -150,7 +150,7 @@ public class GradleProjectClasspath extends JandexClasspath { if (classpathFolder.isPresent()) { return new ParserJavadocProvider(type -> { return SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER - .sourceUrl(classpathFolder.get().toURI().toURL(), type); + .sourceUrl(classpathFolder.get().toURI().toURL(), type.getFullyQualifiedName()); }); } @@ -248,4 +248,9 @@ public class GradleProjectClasspath extends JandexClasspath { return ClasspathData.from(getName(), getClasspathEntries(), getClasspathResources(), getOutputFolder()); } + @Override + public Optional sourceContainer(File classpathResource) { + return Optional.empty(); + } + } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java index 9cd5e5ec0..f7d160015 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.java; import java.io.File; +import java.net.URL; import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.Callable; @@ -178,5 +179,11 @@ public class DelegatingCachedClasspath implements IClasspa t.reindex(); } } + + @Override + public Optional sourceContainer(File classpathResource) { + T t = cachedClasspath.get(); + return t == null ? Optional.empty() : t.sourceContainer(classpathResource); + } } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java index fe9183b8f..5fcfd0951 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.java; import java.io.File; +import java.net.URL; import java.nio.file.Path; import java.util.Optional; import java.util.function.Predicate; @@ -64,4 +65,6 @@ public interface IClasspath { ClasspathData createClasspathData() throws Exception; void reindex(); + + Optional sourceContainer(File classpathResource); } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/javadoc/SourceUrlProviderFromSourceContainer.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/javadoc/SourceUrlProviderFromSourceContainer.java index 631065a75..fd8e0f398 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/javadoc/SourceUrlProviderFromSourceContainer.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/javadoc/SourceUrlProviderFromSourceContainer.java @@ -14,51 +14,54 @@ package org.springframework.ide.vscode.commons.javadoc; import java.net.URL; import java.nio.file.Paths; -import org.springframework.ide.vscode.commons.java.IType; - @FunctionalInterface public interface SourceUrlProviderFromSourceContainer { - public static final SourceUrlProviderFromSourceContainer JAR_SOURCE_URL_PROVIDER = (sourceContainerUrl, type) -> { + static String extractTopLevelType(String fqName) { + int innerTypeIdx = fqName.indexOf('$'); + return innerTypeIdx > 0 ? fqName.substring(0, innerTypeIdx) : fqName; + } + + public static final SourceUrlProviderFromSourceContainer JAR_SOURCE_URL_PROVIDER = (sourceContainerUrl, fqName) -> { StringBuilder sourceUrlStr = new StringBuilder(); sourceUrlStr.append("jar:"); sourceUrlStr.append(sourceContainerUrl); sourceUrlStr.append("!"); sourceUrlStr.append('/'); - sourceUrlStr.append(type.getFullyQualifiedName().replaceAll("\\.", "/")); + sourceUrlStr.append(extractTopLevelType(fqName).replaceAll("\\.", "/")); sourceUrlStr.append(".java"); return new URL(sourceUrlStr.toString()); }; - public static final SourceUrlProviderFromSourceContainer SOURCE_FOLDER_URL_SUPPLIER = (sourceContainerUrl, type) -> { - return Paths.get(sourceContainerUrl.toURI()).resolve(type.getFullyQualifiedName().replaceAll("\\.", "/") + ".java").toUri().toURL(); + public static final SourceUrlProviderFromSourceContainer SOURCE_FOLDER_URL_SUPPLIER = (sourceContainerUrl, fqName) -> { + return Paths.get(sourceContainerUrl.toURI()).resolve(extractTopLevelType(fqName).replaceAll("\\.", "/") + ".java").toUri().toURL(); }; - public static final SourceUrlProviderFromSourceContainer JAR_JAVADOC_URL_PROVIDER = (javadocContainerUrl, type) -> { + public static final SourceUrlProviderFromSourceContainer JAR_JAVADOC_URL_PROVIDER = (javadocContainerUrl, fqName) -> { StringBuilder sourceUrlStr = new StringBuilder(); sourceUrlStr.append("jar:"); sourceUrlStr.append(javadocContainerUrl); sourceUrlStr.append("!"); sourceUrlStr.append('/'); // Inner classes are in separate Top.Nesting1.Nesting2.Nesting3.MyType.html files - sourceUrlStr.append(type.getFullyQualifiedName().replaceAll("\\.", "/").replaceAll("\\$", ".")); + sourceUrlStr.append(fqName.replaceAll("\\.", "/").replaceAll("\\$", ".")); sourceUrlStr.append(".html"); return new URL(sourceUrlStr.toString()); }; - public static final SourceUrlProviderFromSourceContainer JAVADOC_FOLDER_URL_SUPPLIER = (sourceContainerUrl, type) -> { + public static final SourceUrlProviderFromSourceContainer JAVADOC_FOLDER_URL_SUPPLIER = (sourceContainerUrl, fqName) -> { String urlStr = sourceContainerUrl.toString(); StringBuilder sb = new StringBuilder(urlStr); if (!urlStr.endsWith("/")) { sb.append('/'); } // Inner classes are in separate Top.Nesting1.Nesting2.Nesting3.MyType.html files - sb.append(type.getFullyQualifiedName().replaceAll("\\.", "/").replaceAll("\\$", ".") + ".html"); + sb.append(fqName.replaceAll("\\.", "/").replaceAll("\\$", ".") + ".html"); return new URL(sb.toString()); }; - URL sourceUrl(URL sourceContainerUrl, IType type) throws Exception; + URL sourceUrl(URL sourceContainerUrl, String fqName) throws Exception; } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/MavenCore.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/MavenCore.java index 99bf15cbe..9c5580124 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/MavenCore.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/MavenCore.java @@ -95,7 +95,7 @@ public class MavenCore { javaVersion = "8"; } URL javadocUrl = new URL("https://docs.oracle.com/javase/" + javaVersion + "/docs/api/"); - return new HtmlJavadocProvider((type) -> SourceUrlProviderFromSourceContainer.JAVADOC_FOLDER_URL_SUPPLIER.sourceUrl(javadocUrl, type)); + return new HtmlJavadocProvider((type) -> SourceUrlProviderFromSourceContainer.JAVADOC_FOLDER_URL_SUPPLIER.sourceUrl(javadocUrl, type.getFullyQualifiedName())); } catch (MalformedURLException e) { Log.log(e); 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 67764257c..2daa324bd 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 @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -183,6 +184,7 @@ public class MavenProjectClasspath extends JandexClasspath { // }); // } // } + @Override protected IJavadocProvider createParserJavadocProvider(File classpathResource) { @@ -193,34 +195,48 @@ public class MavenProjectClasspath extends JandexClasspath { if (classpathResource.toString().startsWith(cachedData.outputDirectory)) { return new ParserJavadocProvider(type -> { return SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER - .sourceUrl(new File(cachedData.sourceDirectory).toURI().toURL(), type); + .sourceUrl(new File(cachedData.sourceDirectory).toURI().toURL(), type.getFullyQualifiedName()); }); } else if (classpathResource.toString().startsWith(cachedData.testOutputDirectory)) { return new ParserJavadocProvider(type -> { return SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER - .sourceUrl(new File(cachedData.testSourceDirectory).toURI().toURL(), type); + .sourceUrl(new File(cachedData.testSourceDirectory).toURI().toURL(), type.getFullyQualifiedName()); }); } else { throw new IllegalArgumentException("Cannot find source folder for " + classpathResource); } } else { // Assume it's a JAR file - return new ParserJavadocProvider(type -> { + return new ParserJavadocProvider(type -> sourceContainer(classpathResource).map(url -> { try { - Artifact artifact = cachedData.artifacts.stream().filter(a -> classpathResource.equals(a.getFile())).findFirst().get(); - URL sourceContainer = maven.getSources(artifact, cachedData.remoteArtifactRepositories).getFile().toURI().toURL(); - return SourceUrlProviderFromSourceContainer.JAR_SOURCE_URL_PROVIDER.sourceUrl(sourceContainer, - type); - } catch (MavenException e) { - Log.log("Failed to find sources JAR for " + classpathResource, e); - } catch (MalformedURLException e) { - Log.log("Invalid URL for sources JAR for " + classpathResource, e); + return SourceUrlProviderFromSourceContainer.JAR_SOURCE_URL_PROVIDER.sourceUrl(url, + type.getFullyQualifiedName()); + } catch (Exception e) { + Log.log(e); + return null; } - return null; - }); + }).get()); } } + @Override + public Optional sourceContainer(File classpathResource) { + if (cachedData == null) { + return Optional.empty(); + } + return cachedData.artifacts.stream().filter(a -> classpathResource.equals(a.getFile())).findFirst().map(artifact -> { + try { + return maven.getSources(artifact, cachedData.remoteArtifactRepositories).getFile().toURI().toURL(); + } catch (MalformedURLException e) { + Log.log("Invalid URL for sources JAR for " + classpathResource, e); + return null; + } catch (MavenException e) { + Log.log("Failed to find sources JAR for " + classpathResource, e); + return null; + } + }); + } + @Override protected IJavadocProvider createHtmlJavdocProvider(File classpathResource) { if (cachedData == null) { @@ -230,12 +246,12 @@ public class MavenProjectClasspath extends JandexClasspath { if (classpathResource.toString().startsWith(cachedData.outputDirectory)) { return new HtmlJavadocProvider(type -> { return SourceUrlProviderFromSourceContainer.JAVADOC_FOLDER_URL_SUPPLIER - .sourceUrl(new File(cachedData.reportingOutputDirectory, "apidocs").toURI().toURL(), type); + .sourceUrl(new File(cachedData.reportingOutputDirectory, "apidocs").toURI().toURL(), type.getFullyQualifiedName()); }); } else if (classpathResource.toString().startsWith(cachedData.testOutputDirectory)) { return new ParserJavadocProvider(type -> { return SourceUrlProviderFromSourceContainer.JAVADOC_FOLDER_URL_SUPPLIER - .sourceUrl(new File(cachedData.reportingOutputDirectory, "apidocs").toURI().toURL(), type); + .sourceUrl(new File(cachedData.reportingOutputDirectory, "apidocs").toURI().toURL(), type.getFullyQualifiedName()); }); } else { throw new IllegalArgumentException("Cannot find source folder for " + classpathResource); @@ -247,7 +263,7 @@ public class MavenProjectClasspath extends JandexClasspath { Artifact artifact = cachedData.artifacts.stream().filter(a -> classpathResource.equals(a.getFile())).findFirst().get(); URL sourceContainer = maven.getJavadoc(artifact, cachedData.remoteArtifactRepositories).getFile().toURI().toURL(); return SourceUrlProviderFromSourceContainer.JAR_JAVADOC_URL_PROVIDER.sourceUrl(sourceContainer, - type); + type.getFullyQualifiedName()); } catch (MavenException e) { Log.log("Failed to find sources JAR for " + classpathResource, e); } catch (MalformedURLException e) { diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java index 48eb0e3eb..b8313f1f9 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.maven.java.classpathfile; import java.io.File; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; @@ -108,4 +109,9 @@ public class FileClasspath implements IClasspath { @Override public void reindex() { } + + @Override + public Optional sourceContainer(File classpathResource) { + return Optional.empty(); + } }