diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDoc2MarkdownConverter.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDoc2MarkdownConverter.java index f2768ff043..fba50028c2 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDoc2MarkdownConverter.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDoc2MarkdownConverter.java @@ -51,6 +51,7 @@ public class JavaDoc2MarkdownConverter extends AbstractJavaDocConverter { Whitelist w = (Whitelist) whitelistField.get(c); w.addProtocols("a", "href", "file", "jdt"); + w.addProtocols("img", "src", "file"); } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { JavaLanguageServerPlugin.logException("Unable to modify jsoup to include file and jdt protocols", e); } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDocHTMLPathHandler.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDocHTMLPathHandler.java new file mode 100644 index 0000000000..5f8cd7ed18 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDocHTMLPathHandler.java @@ -0,0 +1,341 @@ +/******************************************************************************* + * Copyright (c) 2019 Red Hat Inc. and others. + * 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: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.javadoc; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; + +import org.apache.commons.lang3.SystemUtils; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.dom.TextElement; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; + +/** + * @author Nikolas Komonen - nkomonen@redhat.com + * + */ +public class JavaDocHTMLPathHandler { + + // path to extract images to + public static final String PLUGIN_TEMP_FOLDER = ((SystemUtils.IS_OS_WINDOWS ? "/" : "") + JavaLanguageServerPlugin.getInstance().getStateLocation().toPortableString() + "/").replaceAll(" ", "%20"); + + // absolute path to folder holding all extracted images + public static final IPath EXTRACTED_JAR_IMAGES_FOLDER = new Path(PLUGIN_TEMP_FOLDER).append("extracted-jar-images/"); + + public static final String[] tags = { "img" }; + + public static final Pattern p = Pattern.compile("(src\\s*=\\s*['\"])"); + + /** + * Returns true if the text is an HTML tag in the defined array of tag names in + * {@link JavaDocHTMLPathHandler#tags} + * + * @param text + * @return + */ + public static boolean isHTMLTag(String text) { + + if (!text.startsWith("<") && !text.endsWith(">")) { + return false; + } + + //find index of tag name + int i; + for (i = 1; i < text.length() - 1; i++) { // - 1 to exclude '>' + char c = text.charAt(i); + if (Character.isWhitespace(c)) { + break; + } + } + + if (i == 1) { + return false; + } + + String tagName = text.substring(1, i); + + for (String tag : tags) { + if (tag.equals(tagName)) { + return true; + } + } + + return false; + } + + /** + * Given a {@link TextElement} that represents an HTML tag with a 'src' + * attribute, it will extract the image from the jar if necessary and copy it to + * the 'outputPath'. + * + * @param child + * @param fElement + * @return + */ + public static String getValidatedHTMLSrcAttribute(TextElement child, IJavaElement fElement) { + + //Check if current src attribute path needs to be validated + String text = child.getText(); + int offsets[] = extractSourcePathFromHTMLTag(text); + if (offsets == null) { + return text; + } + String srcPath = text.substring(offsets[0], offsets[1]); + + if (isPathAbsolute(srcPath)) { + return text; //Current path is good as is. + } + + String fileName = Paths.get(srcPath).getFileName().toString(); + + //Get the initial internal jar fragment, which is going to be the relative parent folder to the file + IPackageFragment internalJarFragment = (IPackageFragment) fElement.getAncestor(IJavaElement.PACKAGE_FRAGMENT); + + if (internalJarFragment == null) { + return text; + } + + + //test if srcPath points to the correct file, relative to the fElement file. + IResource fragmentResource = internalJarFragment.getResource(); + if (fragmentResource != null) { + IPath absoluteFragmentPath = fragmentResource.getLocation(); + File absoluteFragmentFile = new File(absoluteFragmentPath.toPortableString() + "/" + srcPath); + if (absoluteFragmentFile.exists()) { + String uriString = "file://" + absoluteFragmentFile.toURI().getPath(); + return text.substring(0, offsets[0]) + uriString + text.substring(offsets[1]); + } + } + + + //folder names are separated by '.' + String fragmentName = internalJarFragment.getElementName(); + + if (fragmentName == null || fragmentName.length() < 1) { + return text; + } + + String[] names = fragmentName.split("\\."); + String fragmentPath = String.join("/", names) + "/"; + + srcPath = srcPath.replace('\\', '/'); + + String filePathRelativeToJar = fragmentPath + srcPath; // /relative/path/to/file.abc + InputStream is = null; // file from jar (ZipEntry) + JarFile jar = null; + + try { + File currentJarPath = null; + + ZipEntry currentZipEntry = null; + + URL javadocJarBaseLocationURL = JavaDocLocations.getJavadocBaseLocation(internalJarFragment); //Absolute location of javadoc jar (not class or source jar) + //Attempt to get file from javadoc jar + if (javadocJarBaseLocationURL != null) { + URI javadocJarBaseLocationURI = javadocJarBaseLocationURL.toURI(); + currentJarPath = getJarPathFromURI(javadocJarBaseLocationURI); + if (currentJarPath != null) { + jar = new JarFile(currentJarPath); + currentZipEntry = jar.getEntry(filePathRelativeToJar); + } + } + + //No file was in the javadoc jar, try the source jar + if (currentZipEntry == null) { + currentJarPath = SourceJarLocations.getSourceJarPath(internalJarFragment); //Absolute location of source jar + if (currentJarPath != null) { + jar = new JarFile(currentJarPath); + currentZipEntry = jar.getEntry(filePathRelativeToJar); + } + } + + if (jar == null || currentZipEntry == null) { + return text; //File from source path could not be located in either jar + } + + + + String jarRootName = internalJarFragment.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT).getElementName(); + if (jarRootName.endsWith(".jar")) { + jarRootName = jarRootName.substring(0, jarRootName.length() - 4); + } + jarRootName += "/"; + + IPath newOutputPath = EXTRACTED_JAR_IMAGES_FOLDER.append(jarRootName); + newOutputPath = newOutputPath.append(fileName); + File outputFile = newOutputPath.toFile(); + + //Check if the file actually needs to be extracted + if (!outputFile.exists()) { + is = jar.getInputStream(currentZipEntry); + extractFileTo(is, outputFile); + } else {//Check if the file is outdated + BasicFileAttributes existingOutputFileAttributes = Files.readAttributes(outputFile.toPath(), BasicFileAttributes.class); + FileTime existingFileCreationTime = existingOutputFileAttributes.creationTime(); + BasicFileAttributes jarOutputFileAttributes = Files.readAttributes(currentJarPath.toPath(), BasicFileAttributes.class); + + FileTime jarFileCreationTime = jarOutputFileAttributes.creationTime(); + + if (jarFileCreationTime.compareTo(existingFileCreationTime) > 0) { + is = jar.getInputStream(currentZipEntry); + extractFileTo(is, outputFile); + } + } + + //Insert new path into text + return text.substring(0, offsets[0]) + outputFile.toURI().toString() + text.substring(offsets[1]); + + } catch (Exception e) { + JavaLanguageServerPlugin.logException("Failed to extract path from embedded jar with error message:", e); + return text; + } finally { + //cleanup + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + if (jar != null) { + try { + jar.close(); + } catch (IOException e) { + } + } + } + } + + /** + * Gets the position between the quotes of a src attribute. Will look for + * something similar to (src="...") and extract the path from inside. + * + * Start offset is at offsets[0], after the start quotation. End offset is at + * offsets[1], before the end quotation. + * + * Offsets are at '|': + * + * src="|nikolas/wrote/this|" + * + * If the src attribute cannot be found, null is returned. + * + * @param text + * @return int[] with start and end offset of src attribute value, else null. + */ + public static int[] extractSourcePathFromHTMLTag(String text) { + Matcher m = p.matcher(text); + if (m.find()) { + int srcStartQuote = m.end(); + char quote = text.charAt(srcStartQuote - 1); + int srcEndQuote = text.indexOf(quote, srcStartQuote); + int[] offsets = { srcStartQuote, srcEndQuote }; + return offsets; + } + return null; + } + + public static File getJarPathFromURI(URI uri) { + String pathWithScheme = uri.getSchemeSpecificPart(); + String finalJarRootPath = pathWithScheme.substring(pathWithScheme.indexOf(':') + 1); + + //clean up/verify the jar path + int actualJarIndex = finalJarRootPath.lastIndexOf(".jar"); + + if (actualJarIndex == -1) { + return null; + } + + return new File(finalJarRootPath.substring(0, actualJarIndex + 4)); + } + + /** + * Checks if a given path is absolute. This path must be in the format of a URI + * (with scheme) or a Unix like path. + * + * eg: + * + * Linux: file:///home/nikolas/file.txt Windows: + * file:///C:/Users/Nikolas/file.txt + * + * If the path is not in the URI format, it will always return false since the + * scheme is missing. + * + * @param path + * in format of URI + * @return true if path/URI is absolute + */ + public static boolean isPathAbsolute(String path) { + try { + + URI uri = new URI(path); + + if (uri.isAbsolute()) { + return true; + } + + File f = new File(path); + + if (f.isAbsolute()) { + return true; + } + + //The case we are on Windows and we get a Unix-like path + if (path.startsWith("/")) { + return true; + } + + + return false; + + } catch (URISyntaxException e) { + return false; + } + } + + /** + * Given an inputstream, outputs a file to the given path + * + * @param fileToExtract + * @param pathToExtractTo + * @return + */ + private static boolean extractFileTo(InputStream fileToExtract, File pathToExtractTo) { + + pathToExtractTo.getParentFile().mkdirs(); + + try (FileOutputStream os = new FileOutputStream(pathToExtractTo);) { + int c; + while ((c = fileToExtract.read()) != -1) { + os.write(c); + } + } catch (IOException e) { + JavaLanguageServerPlugin.logException("Failed to extract file with error message: ", e); + return false; + } + return true; + } +} diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavadocContentAccess2.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavadocContentAccess2.java index e1749bea06..c6c869d998 100644 --- a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavadocContentAccess2.java +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/JavadocContentAccess2.java @@ -1461,6 +1461,10 @@ private void handleContentElements(List nodes, boolean skipLe previousNode = child; if (child instanceof TextElement) { String text = ((TextElement) child).getText(); + if (JavaDocHTMLPathHandler.isHTMLTag(text)) { + text = JavaDocHTMLPathHandler.getValidatedHTMLSrcAttribute((TextElement) child, fElement); + } + if (skipLeadingWhitespace) { text = text.replaceFirst("^\\s", ""); //$NON-NLS-1$ //$NON-NLS-2$ } diff --git a/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/SourceJarLocations.java b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/SourceJarLocations.java new file mode 100644 index 0000000000..def7afd7a4 --- /dev/null +++ b/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/javadoc/SourceJarLocations.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2019 Red Hat Inc. and others. + * 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: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.javadoc; + +import java.io.File; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.corext.util.JavaModelUtil; + +/** + * @author Nikolas Komonen - nkomonen@redhat.com + * + */ +public class SourceJarLocations { + + + public static File getSourceJarPath(IJavaElement element) throws JavaModelException { + IPackageFragmentRoot root = JavaModelUtil.getPackageFragmentRoot(element); + + if (root == null) { + return null; + } + + IClasspathEntry entry = root.getResolvedClasspathEntry(); + IPath sourceAttachment = entry.getSourceAttachmentPath(); + + if (sourceAttachment == null) { + return null; //No source jar could be found + } + + return sourceAttachment.toFile(); + } +} diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-hyperlink/pom.xml b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-hyperlink/pom.xml new file mode 100644 index 0000000000..fd8457592a --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-hyperlink/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + foo.bar + javadoc-image-extraction-with-hyperlink + 0.0.1-SNAPSHOT + + + + + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-eclipse-plugin + + + + + + + \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-hyperlink/src/main/java/foo/JavaDocJarTest.java b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-hyperlink/src/main/java/foo/JavaDocJarTest.java new file mode 100644 index 0000000000..0fb8fb4c65 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-hyperlink/src/main/java/foo/JavaDocJarTest.java @@ -0,0 +1,18 @@ +package foo; + +/** + * WARNING: DO NOT ADJUST FILE, IT WILL MESS UP OFFSETS + * @author nkomonen + * + */ +public class JavaDocJarTest { + /** + * If this test fails, check if link is still valid. + * + * + * @param args + */ + public static void main(String[] args) { + + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-javadoc/pom.xml b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-javadoc/pom.xml new file mode 100644 index 0000000000..8e5c1dfa48 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-javadoc/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + foo.bar + javadoc-image-extraction-with-javadoc + 0.0.1-SNAPSHOT + + + + + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + + + + + + + + io.projectreactor + reactor-test + 3.2.10.RELEASE + + + + \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-javadoc/src/main/java/foo/JavaDocJarTest.java b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-javadoc/src/main/java/foo/JavaDocJarTest.java new file mode 100644 index 0000000000..ab8369f886 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-javadoc/src/main/java/foo/JavaDocJarTest.java @@ -0,0 +1,13 @@ +package foo; + +import reactor.core.publisher.Mono; +/** + * WARNING: DO NOT ADJUST FILE, IT WILL MESS UP OFFSETS + * @author nkomonen + * + */ +public class JavaDocJarTest { + public static void main(String[] args) { + Mono.error(new Exception("Broke")); + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-sources/pom.xml b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-sources/pom.xml new file mode 100644 index 0000000000..a49e2e5077 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-sources/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + foo.bar + javadoc-image-extraction-with-sources + 0.0.1-SNAPSHOT + + + + + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-eclipse-plugin + + true + + + + + + + + + io.projectreactor + reactor-test + 3.2.10.RELEASE + + + + \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-sources/src/main/java/foo/JavaDocJarTest.java b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-sources/src/main/java/foo/JavaDocJarTest.java new file mode 100644 index 0000000000..ab8369f886 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-with-sources/src/main/java/foo/JavaDocJarTest.java @@ -0,0 +1,13 @@ +package foo; + +import reactor.core.publisher.Mono; +/** + * WARNING: DO NOT ADJUST FILE, IT WILL MESS UP OFFSETS + * @author nkomonen + * + */ +public class JavaDocJarTest { + public static void main(String[] args) { + Mono.error(new Exception("Broke")); + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-without-any/pom.xml b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-without-any/pom.xml new file mode 100644 index 0000000000..453320bb4f --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-without-any/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + foo.bar + javadoc-image-extraction-without-any + 0.0.1-SNAPSHOT + + + + + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-eclipse-plugin + + + + + + + \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-without-any/src/main/java/foo/JavaDocJarTest.java b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-without-any/src/main/java/foo/JavaDocJarTest.java new file mode 100644 index 0000000000..8d9468e6ef --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/javadoc-image-extraction-without-any/src/main/java/foo/JavaDocJarTest.java @@ -0,0 +1,16 @@ +package foo; + +/** + * WARNING: DO NOT ADJUST FILE, IT WILL MESS UP OFFSETS + * @author nkomonen + * + */ +public class JavaDocJarTest { + /** + * + * @param args + */ + public static void main(String[] args) { + + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/relative-image/pom.xml b/org.eclipse.jdt.ls.tests/projects/maven/relative-image/pom.xml new file mode 100644 index 0000000000..88b8e23cc1 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/relative-image/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + foo.bar + relative-image + 0.0.1-SNAPSHOT + + + + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + + + + + commons-primitives + commons-primitives + 1.0 + + + \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/projects/maven/relative-image/src/main/java/foo/bar/FolderWithPictures/red-hat-logo.png b/org.eclipse.jdt.ls.tests/projects/maven/relative-image/src/main/java/foo/bar/FolderWithPictures/red-hat-logo.png new file mode 100644 index 0000000000..75fe247f4f Binary files /dev/null and b/org.eclipse.jdt.ls.tests/projects/maven/relative-image/src/main/java/foo/bar/FolderWithPictures/red-hat-logo.png differ diff --git a/org.eclipse.jdt.ls.tests/projects/maven/relative-image/src/main/java/foo/bar/RelativeImage.java b/org.eclipse.jdt.ls.tests/projects/maven/relative-image/src/main/java/foo/bar/RelativeImage.java new file mode 100644 index 0000000000..af3de831e5 --- /dev/null +++ b/org.eclipse.jdt.ls.tests/projects/maven/relative-image/src/main/java/foo/bar/RelativeImage.java @@ -0,0 +1,11 @@ +package foo.bar; + +public class RelativeImage { + + /** + * + */ + public static void testingMethod() { + + } +} \ No newline at end of file diff --git a/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDocImageExtractionTest.java b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDocImageExtractionTest.java new file mode 100644 index 0000000000..c54472174a --- /dev/null +++ b/org.eclipse.jdt.ls.tests/src/org/eclipse/jdt/ls/core/internal/javadoc/JavaDocImageExtractionTest.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * Copyright (c) 2019 Red Hat Inc. and others. + * 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: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.ls.core.internal.javadoc; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IClassFile; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.internal.core.BinaryType; +import org.eclipse.jdt.ls.core.internal.HoverInfoProvider; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.JobHelpers; +import org.eclipse.jdt.ls.core.internal.SourceContentProvider; +import org.eclipse.jdt.ls.core.internal.WorkspaceHelper; +import org.eclipse.jdt.ls.core.internal.managers.AbstractMavenBasedTest; +import org.junit.After; +import org.junit.Test; + +/** + * The purpose of this test is that the LS needs to extract images embedded in a + * source/javadoc jar if they are being referenced in a javadoc + * tag. + * + * Normally it would work given a path relative to the jar, but the LSP does not + * know that, and we must provide an absolute path. + * + * Initial Issue: https://github.com/redhat-developer/vscode-java/issues/1007 + * + * + * The test projects can be found in org.eclipse.jdt.ls.tests/projects/maven/... + * + * @author nkomonen + * + */ +public class JavaDocImageExtractionTest extends AbstractMavenBasedTest { + + private IProject project; + + private String testFolderName; + + @After + public void cleanup() throws Exception { + testFolderName = null; + project = null; + } + + @Test + public void testImageExtractionWithSourceJar() throws Exception { + testFolderName = "javadoc-image-extraction-with-sources"; + helpTestImageExtractionWithXJar(testFolderName); + } + + @Test + public void testImageExtractionWithJavadocJar() throws Exception { + testFolderName = "javadoc-image-extraction-with-javadoc"; + helpTestImageExtractionWithXJar(testFolderName); + } + + public void helpTestImageExtractionWithXJar(String testFolderName) throws Exception { + setupMockMavenProject(testFolderName, "reactor.core.publisher.Mono"); + + URI uri = project.getFile("src/main/java/foo/JavaDocJarTest.java").getLocationURI(); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri); + + assertTrue(cu.isStructureKnown()); + + IJavaElement javaElement = JDTUtils.findElementAtSelection(cu, 10, 12, null, new NullProgressMonitor()); + + + String testExportPath = JavaDocHTMLPathHandler.EXTRACTED_JAR_IMAGES_FOLDER + "reactor-core-3.2.10.RELEASE/error.svg"; + + //@formatter:off + // We don't use this. Just to show what it should look like + String expectedOutput = + "Create a [Mono](jdt://contents/reactor-core-3.2.10.RELEASE.jar/reactor.core.publisher/Mono.class?=javadoctest/%5C/home%5C/nkomonen%5C/.m2%5C/repository%5C/io%5C/projectreactor%5C/reactor-core%5C/3.2.10.RELEASE%5C/reactor-core-3.2.10.RELEASE.jar%3Creactor.core.publisher%28Mono.class#101) that terminates with the specified error immediately after being subscribed to.\n" + + "\n" + + "![Image](file:" + testExportPath + ")\n" + + "\n" + + " * **Type Parameters:**\n" + + " \n" + + " * **** the reified [Subscriber](jdt://contents/reactive-streams-1.0.2.jar/org.reactivestreams/Subscriber.class?=javadoctest/%5C/home%5C/nkomonen%5C/.m2%5C/repository%5C/org%5C/reactivestreams%5C/reactive-streams%5C/1.0.2%5C/reactive-streams-1.0.2.jar%3Corg.reactivestreams%28Subscriber.class#29) type\n" + + " * **Parameters:**\n" + + " \n" + + " * **error** the onError signal\n" + + " * **Returns:**\n" + + " \n" + + " * a failing [Mono](jdt://contents/reactor-core-3.2.10.RELEASE.jar/reactor.core.publisher/Mono.class?=javadoctest/%5C/home%5C/nkomonen%5C/.m2%5C/repository%5C/io%5C/projectreactor%5C/reactor-core%5C/3.2.10.RELEASE%5C/reactor-core-3.2.10.RELEASE.jar%3Creactor.core.publisher%28Mono.class#101)"; + //@formatter:on + + String expectedImageMarkdown = "![Image](file:" + testExportPath + ")"; + + String finalString = HoverInfoProvider.computeJavadoc(javaElement).getValue(); + + assertTrue("Does finalString=\n\t\"" + finalString + "\"\nContain expectedImageMarkdown=\n\t\"" + expectedImageMarkdown + "\"", finalString.contains(expectedImageMarkdown)); + + assertTrue(new File(testExportPath).exists()); + } + + @Test + public void testImageExtractionWithoutAnyJars() throws Exception { + testFolderName = "javadoc-image-extraction-without-any"; + setupMockMavenProject(testFolderName); + + URI uri = project.getFile("src/main/java/foo/JavaDocJarTest.java").getLocationURI(); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri); + + assertTrue(cu.isStructureKnown()); + + IJavaElement javaElement = JDTUtils.findElementAtSelection(cu, 12, 22, null, new NullProgressMonitor()); + String finalString = HoverInfoProvider.computeJavadoc(javaElement).getValue(); + + String expectedImageMarkdown = "![Image]()"; + + assertTrue(finalString.contains(expectedImageMarkdown)); + + } + + @Test + public void testImageRelativeToFile() throws Exception { + + testFolderName = "relative-image"; + setupMockMavenProject(testFolderName); + + + URI uri = project.getFile("src/main/java/foo/bar/RelativeImage.java").getLocationURI(); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri); + + assertTrue(cu.isStructureKnown()); + + IJavaElement javaElement = JDTUtils.findElementAtSelection(cu, 7, 23, null, new NullProgressMonitor()); + + Path pp = Paths.get(uri).getParent(); + URI parentURI = pp.toUri(); + + String parentExportPath = parentURI.getPath(); + + String relativeExportPath = "FolderWithPictures/red-hat-logo.png"; + + String absoluteExportPath = parentExportPath + relativeExportPath; + + String expectedImageMarkdown = "![Image](file:" + absoluteExportPath + ")"; + + String finalString = HoverInfoProvider.computeJavadoc(javaElement).getValue(); + + assertTrue(finalString.contains(expectedImageMarkdown)); + + } + + @Test + public void testHyperlinkImage() throws Exception { + testFolderName = "javadoc-image-extraction-with-hyperlink"; + setupMockMavenProject(testFolderName); + + URI uri = project.getFile("src/main/java/foo/JavaDocJarTest.java").getLocationURI(); + ICompilationUnit cu = JDTUtils.resolveCompilationUnit(uri); + + assertTrue(cu.isStructureKnown()); + + IJavaElement javaElement = JDTUtils.findElementAtSelection(cu, 14, 22, null, new NullProgressMonitor()); + String finalString = HoverInfoProvider.computeJavadoc(javaElement).getValue(); + + String expectedImageMarkdown = "![Image](https://www.redhat.com/cms/managed-files/Logo-redhat-color-375.png)"; + + assertTrue(finalString.contains(expectedImageMarkdown)); + } + + @Test + public void testIsAbsolutePath() { + + assertTrue(JavaDocHTMLPathHandler.isPathAbsolute("/usr/nikolas/file.txt")); + assertTrue(JavaDocHTMLPathHandler.isPathAbsolute("file:/usr/nikolas/file.txt")); + assertTrue(JavaDocHTMLPathHandler.isPathAbsolute("file:///usr/nikolas/file.txt")); + assertTrue(JavaDocHTMLPathHandler.isPathAbsolute("https://nikolas.com/file.txt")); + + assertFalse(JavaDocHTMLPathHandler.isPathAbsolute("usr/nikolas/file.txt")); + assertFalse(JavaDocHTMLPathHandler.isPathAbsolute("usr/nikolas/folder/")); + } + + //******* Utils ******* + + public void setupMockMavenProject(String folderName) throws Exception { + setupMockMavenProject(folderName, null); + } + + public void setupMockMavenProject(String folderName, String... classpathNames) throws Exception { + setupMockXProject("maven", folderName, classpathNames); + + } + + public void setupMockEclipseProject(String folderName) throws Exception { + setupMockXProject("eclipse", folderName); + } + + public void setupMockXProject(String projectTypeFolderName, String projectFolderName, String... classpathNames) throws Exception { + importProjects(projectTypeFolderName + "/" + projectFolderName); + project = WorkspaceHelper.getProject(projectFolderName); + + if (classpathNames != null) { + for (String classpathName : classpathNames) { + if (classpathName != null) { + ensureSourceOfClassIsDownloaded(classpathName); + } + } + } + } + + public void ensureSourceOfClassIsDownloaded(String classpathName) throws Exception { + IJavaProject javaProject = JavaCore.create(project); + IType type = javaProject.findType(classpathName); //eg: reactor.core.publisher.Mono + IClassFile classFile = ((BinaryType) type).getClassFile(); + String source = new SourceContentProvider().getSource(classFile, new NullProgressMonitor()); + if (source == null) { + JobHelpers.waitForDownloadSourcesJobs(JobHelpers.MAX_TIME_MILLIS); + source = new SourceContentProvider().getSource(classFile, new NullProgressMonitor()); + } + assertNotNull(source); + } +}