diff --git a/java/java-tests/testData/codeInsight/documentation/inherit.gif b/java/java-tests/testData/codeInsight/documentation/inherit.gif new file mode 100644 index 000000000000..c814867a13de Binary files /dev/null and b/java/java-tests/testData/codeInsight/documentation/inherit.gif differ diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/documentation/RefConvertorsTest.java b/java/java-tests/testSrc/com/intellij/codeInsight/documentation/RefConvertorsTest.java new file mode 100644 index 000000000000..f5bd537a6788 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/codeInsight/documentation/RefConvertorsTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed 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. + */ +package com.intellij.codeInsight.documentation; + +import com.intellij.JavaTestUtil; +import com.intellij.codeInsight.javadoc.JavaDocExternalFilter; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.openapi.vfs.JarFileSystem; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.testFramework.LightCodeInsightTestCase; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * @author Denis Zhdanov + * @since 1/15/13 7:26 PM + */ +public class RefConvertorsTest extends LightCodeInsightTestCase { + + private File myExtractedImagesDir; + + @Override + protected void setUp() throws Exception { + super.setUp(); + String tempDirectory = FileUtilRt.getTempDirectory(); + myExtractedImagesDir = new File(tempDirectory, AbstractExternalFilter.QUICK_DOC_DIR_NAME); + } + + @Override + protected void tearDown() throws Exception { + FileUtilRt.delete(myExtractedImagesDir); + super.tearDown(); + } + + public void testImgInsideJar() throws Exception { + String imgJarName = "test-img"; + File imgJar = new File(myExtractedImagesDir, imgJarName + ".jar"); + boolean exist = FileUtil.createIfDoesntExist(imgJar); + assertTrue(exist); + + JarOutputStream out = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(imgJar))); + try { + out.putNextEntry(new JarEntry("resources/inherit.gif")); + FileInputStream fIn = new FileInputStream(JavaTestUtil.getJavaTestDataPath() + "/codeInsight/documentation/inherit.gif"); + try { + FileUtil.copy(fIn, out); + } + finally { + fIn.close(); + } + } + finally { + out.close(); + } + + String textBefore = + "" + + "java.lang.Object\n" + + " \"extendedorg.bouncycastle.asn1.BERSequenceParser\n" + + ""; + + File f = new File(myExtractedImagesDir, imgJarName); + f = new File(f, "resources"); + File extractedImgFile = new File(f, "inherit.gif"); + String expectedTextAfter = String.format( + "" + + "java.lang.Object\n" + + " \"extendedorg.bouncycastle.asn1.BERSequenceParser\n" + + "", + LocalFileSystem.PROTOCOL_PREFIX, + extractedImgFile.getAbsolutePath()); + + JavaDocExternalFilter filter = new JavaDocExternalFilter(getProject()); + String textAfter = filter.correctRefs( + String.format("%s%s!/org/bouncycastle/asn1/BERSequenceParser.html", JarFileSystem.PROTOCOL_PREFIX, imgJar.getAbsolutePath()), + textBefore + ); + assertEquals(expectedTextAfter, textAfter); + assertTrue(extractedImgFile.isFile()); + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/documentation/AbstractExternalFilter.java b/platform/lang-impl/src/com/intellij/codeInsight/documentation/AbstractExternalFilter.java index 235beb204146..681e8e4393d0 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/documentation/AbstractExternalFilter.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/documentation/AbstractExternalFilter.java @@ -24,12 +24,12 @@ import com.intellij.openapi.progress.util.ProgressIndicatorBase; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Trinity; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VfsUtilCore; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.*; import com.intellij.psi.PsiElement; +import com.intellij.util.SystemProperties; import com.intellij.util.io.UrlConnectionUtil; import com.intellij.util.net.HttpConfigurable; import org.jetbrains.annotations.NonNls; @@ -52,35 +52,45 @@ import java.util.regex.Pattern; */ public abstract class AbstractExternalFilter { + + private static final boolean EXTRACT_IMAGES_FROM_JARS = SystemProperties.getBooleanProperty("extract.doc.images", true); + + @NotNull public static final String QUICK_DOC_DIR_NAME = "quickdoc"; + private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.javadoc.JavaDocExternalFilter"); - - private static final Pattern ourClassDataStartPattern = Pattern.compile("START OF CLASS DATA", Pattern.CASE_INSENSITIVE); - private static final Pattern ourClassDataEndPattern = Pattern.compile("SUMMARY ========", Pattern.CASE_INSENSITIVE); + + private static final Pattern ourClassDataStartPattern = Pattern.compile("START OF CLASS DATA", Pattern.CASE_INSENSITIVE); + private static final Pattern ourClassDataEndPattern = Pattern.compile("SUMMARY ========", Pattern.CASE_INSENSITIVE); private static final Pattern ourNonClassDataEndPattern = Pattern.compile("]*)\"", Pattern.CASE_INSENSITIVE|Pattern.DOTALL); - private static @NonNls final String JAR_PROTOCOL = "jar:"; - @NonNls private static final String HR = "
"; - @NonNls private static final String P = "

"; - @NonNls private static final String DL = "

"; - @NonNls protected static final String H2 = ""; - @NonNls protected static final String HTML_CLOSE = ""; - @NonNls protected static final String HTML = ""; - @NonNls private static final String BR = "
"; - @NonNls private static final String DT = "
"; - private static final Pattern CHARSET_META_PATTERN = + + protected static @NonNls final Pattern ourAnchorsuffix = Pattern.compile("#(.*)$"); + protected static @NonNls final Pattern ourHTMLFilesuffix = Pattern.compile("/([^/]*[.][hH][tT][mM][lL]?)$"); + private static @NonNls final Pattern ourAnnihilator = Pattern.compile("/[^/^.]*/[.][.]/"); + private static @NonNls final Pattern ourIMGselector = + Pattern.compile("]*?)\"", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private static @NonNls final Pattern ourPathInsideJarPattern = Pattern.compile( + String.format("%s(.+\\.jar)!/(.+?)[^/]+", JarFileSystem.PROTOCOL_PREFIX), + Pattern.CASE_INSENSITIVE | Pattern.DOTALL + ); + private static @NonNls final String JAR_PROTOCOL = "jar:"; + @NonNls private static final String HR = "
"; + @NonNls private static final String P = "

"; + @NonNls private static final String DL = "

"; + @NonNls protected static final String H2 = ""; + @NonNls protected static final String HTML_CLOSE = ""; + @NonNls protected static final String HTML = ""; + @NonNls private static final String BR = "
"; + @NonNls private static final String DT = "
"; + private static final Pattern CHARSET_META_PATTERN = Pattern.compile("]+\\s*charset=\"?([\\w\\-]*)\\s*\">", Pattern.CASE_INSENSITIVE); - private static final String FIELD_SUMMARY = ""; - private static final String CLASS_SUMMARY = "
"; - private final HttpConfigurable myHttpConfigurable = HttpConfigurable.getInstance(); + private static final String FIELD_SUMMARY = ""; + private static final String CLASS_SUMMARY = "
"; + private final HttpConfigurable myHttpConfigurable = HttpConfigurable.getInstance(); protected static abstract class RefConvertor { - private final Pattern mySelector; + @NotNull private final Pattern mySelector; - public RefConvertor(Pattern selector) { + public RefConvertor(@NotNull Pattern selector) { mySelector = selector; } @@ -99,12 +109,12 @@ public abstract class AbstractExternalFilter { ready.append(before); ready.append("\""); ready.append(ApplicationManager.getApplication().runReadAction( - new Computable() { - @Override - public String compute() { - return convertReference(root, href); - } + new Computable() { + @Override + public String compute() { + return convertReference(root, href); } + } )); ready.append("\""); } @@ -119,10 +129,63 @@ public abstract class AbstractExternalFilter { @Override protected String convertReference(String root, String href) { if (StringUtil.startsWithChar(href, '#')) { - return DocumentationManager.DOC_ELEMENT_PROTOCOL + root + href; + return DocumentationManagerProtocol.DOC_ELEMENT_PROTOCOL + root + href; } - if (Comparing.strEqual(VirtualFileManager.extractProtocol(root), LocalFileSystem.PROTOCOL)) { + String protocol = VirtualFileManager.extractProtocol(root); + if (EXTRACT_IMAGES_FROM_JARS && Comparing.strEqual(protocol, JarFileSystem.PROTOCOL)) { + Matcher matcher = ourPathInsideJarPattern.matcher(root); + if (matcher.matches()) { + // There is a possible case that javadoc jar is assembled with images inside. However, our standard quick doc + // renderer (JEditorPane) doesn't know how to reference images from such jars. That's why we unpack them to temp + // directory if necessary and substitute that 'inside jar path' to usual file url. + String jarPath = matcher.group(1); + String jarName = jarPath; + int i = jarName.lastIndexOf(File.separatorChar); + if (i >= 0 && i < jarName.length() - 1) { + jarName = jarName.substring(i + 1); + } + jarName = jarName.substring(0, jarName.length() - ".jar".length()); + String basePath = matcher.group(2); + String imgPath = FileUtil.toCanonicalPath(basePath + href); + File unpackedImagesRoot = new File(FileUtilRt.getTempDirectory(), QUICK_DOC_DIR_NAME); + File unpackedJarImagesRoot = new File(unpackedImagesRoot, jarName); + File unpackedImage = new File(unpackedJarImagesRoot, imgPath); + boolean referenceUnpackedImage = true; + if (!unpackedImage.isFile()) { + referenceUnpackedImage = false; + JarFileSystem jarFileSystem = JarFileSystem.getInstance(); + try { + JarFile jarFile = jarFileSystem.getJarFile(jarFileSystem.findFileByPath(jarPath + StandardFileSystems.JAR_SEPARATOR)); + if (jarFile != null) { + JarFile.JarEntry entry = jarFile.getEntry(imgPath); + if (entry != null) { + FileUtilRt.createIfNotExists(unpackedImage); + FileOutputStream fOut = new FileOutputStream(unpackedImage); + try { + // Don't bother with wrapping file output stream into buffered stream in assumption that FileUtil operates + // on NIO channels. + FileUtilRt.copy(jarFile.getInputStream(entry), fOut); + referenceUnpackedImage = true; + } + finally { + fOut.close(); + } + } + unpackedImage.deleteOnExit(); + } + } + catch (IOException e) { + // Do nothing + } + } + if (referenceUnpackedImage) { + return LocalFileSystem.PROTOCOL_PREFIX + unpackedImage.getAbsolutePath(); + } + } + } + + if (Comparing.strEqual(protocol, LocalFileSystem.PROTOCOL)) { final String path = VirtualFileManager.extractPath(root); if (!path.startsWith("/")) {//skip host for local file system files (format - file://host_name/path) root = VirtualFileManager.constructUrl(LocalFileSystem.PROTOCOL, "/" + path); @@ -143,7 +206,7 @@ public abstract class AbstractExternalFilter { return path; } - private String correctRefs(String root, String read) { + public String correctRefs(String root, String read) { String result = read; for (RefConvertor myReferenceConvertor : getRefConvertors()) { @@ -156,7 +219,9 @@ public abstract class AbstractExternalFilter { protected abstract RefConvertor[] getRefConvertors(); @Nullable - private static Reader getReaderByUrl(final String surl, final HttpConfigurable httpConfigurable, final ProgressIndicator pi) throws IOException { + private static Reader getReaderByUrl(final String surl, final HttpConfigurable httpConfigurable, final ProgressIndicator pi) + throws IOException + { if (surl.startsWith(JAR_PROTOCOL)) { VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(BrowserUtil.getDocURL(surl)); @@ -188,7 +253,7 @@ public abstract class AbstractExternalFilter { if (result != null) return result; //noinspection IOResourceOpenedButNotSafelyClosed reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - for(String htmlLine = reader.readLine();htmlLine != null; htmlLine = reader.readLine()) { + for (String htmlLine = reader.readLine(); htmlLine != null; htmlLine = reader.readLine()) { result = parseContentEncoding(htmlLine); if (result != null) { break; @@ -196,12 +261,14 @@ public abstract class AbstractExternalFilter { } } catch (IOException ignored) { - } finally { + } + finally { if (reader != null) try { reader.close(); } - catch (IOException ignored) {} + catch (IOException ignored) { + } } return result; } @@ -209,7 +276,7 @@ public abstract class AbstractExternalFilter { @Nullable @SuppressWarnings({"HardCodedStringLiteral"}) public String getExternalDocInfo(final String surl) throws Exception { - if (surl == null) return null; + if (surl == null) return null; if (MyJavadocFetcher.isFree()) { final MyJavadocFetcher fetcher = new MyJavadocFetcher(surl, new MyDocBuilder() { @Override @@ -240,8 +307,8 @@ public abstract class AbstractExternalFilter { } @Nullable - public String getExternalDocInfoForElement(final String docURL, final PsiElement element) throws Exception { - return getExternalDocInfo(docURL); + public String getExternalDocInfoForElement(final String docURL, final PsiElement element) throws Exception { + return getExternalDocInfo(docURL); } protected void doBuildFromStream(String surl, Reader input, StringBuffer data) throws IOException { @@ -257,21 +324,21 @@ public abstract class AbstractExternalFilter { @NonNls String greatestEndSection = ""; data.append(HTML); - data.append( ""); + data.append(""); String read; String contentEncoding = null; @@ -288,7 +355,8 @@ public abstract class AbstractExternalFilter { if (input instanceof MyReader && contentEncoding != null) { if (!contentEncoding.equalsIgnoreCase("UTF-8") && - !contentEncoding.equals(((MyReader)input).getEncoding())) { //restart page parsing with correct encoding + !contentEncoding.equals(((MyReader)input).getEncoding())) + { //restart page parsing with correct encoding Reader stream; try { stream = getReaderByUrl(surl, myHttpConfigurable, new ProgressIndicatorBase());