/* * Copyright 2000-2016 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.jarFinder; import com.intellij.notification.Notification; import com.intellij.notification.NotificationType; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.roots.LibraryOrderEntry; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.ui.configuration.LibrarySourceRootDetectorUtil; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.JarFileSystem; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.util.SystemProperties; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.io.HttpRequests; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.util.*; import java.util.regex.Pattern; /** * @author Sergey Evdokimov */ public class InternetAttachSourceProvider extends AbstractAttachSourceProvider { private static final Logger LOG = Logger.getInstance(InternetAttachSourceProvider.class); private static final Pattern ARTIFACT_IDENTIFIER = Pattern.compile("[A-Za-z0-9\\.\\-_]+"); @NotNull @Override public Collection<AttachSourcesAction> getActions(List<LibraryOrderEntry> orderEntries, @Nullable PsiFile psiFile) { final VirtualFile jar = getJarByPsiFile(psiFile); if (jar == null) return Collections.emptyList(); final String jarName = jar.getNameWithoutExtension(); int index = jarName.lastIndexOf('-'); if (index == -1) return Collections.emptyList(); final String version = jarName.substring(index + 1); final String artifactId = jarName.substring(0, index); if (!ARTIFACT_IDENTIFIER.matcher(version).matches() || !ARTIFACT_IDENTIFIER.matcher(artifactId).matches()) { return Collections.emptyList(); } final Set<Library> libraries = new HashSet<>(); for (LibraryOrderEntry orderEntry : orderEntries) { ContainerUtil.addIfNotNull(libraries, orderEntry.getLibrary()); } if (libraries.isEmpty()) return Collections.emptyList(); final String sourceFileName = jarName + "-sources.jar"; for (Library library : libraries) { for (VirtualFile file : library.getFiles(OrderRootType.SOURCES)) { if (file.getPath().contains(sourceFileName)) { if (isRootInExistingFile(file)) { return Collections.emptyList(); // Sources already attached, but source-jar doesn't contain current class. } } } } final File libSourceDir = getLibrarySourceDir(); final File sourceFile = new File(libSourceDir, sourceFileName); if (sourceFile.exists()) { return Collections.singleton(new LightAttachSourcesAction() { @Override public String getName() { return "Attach downloaded source"; } @Override public String getBusyText() { return getName(); } @Override public ActionCallback perform(List<LibraryOrderEntry> orderEntriesContainingFile) { attachSourceJar(sourceFile, libraries); return ActionCallback.DONE; } }); } return Collections.singleton(new LightAttachSourcesAction() { @Override public String getName() { return "Download..."; } @Override public String getBusyText() { return "Searching..."; } @Override public ActionCallback perform(List<LibraryOrderEntry> orderEntriesContainingFile) { final Task task = new Task.Modal(psiFile.getProject(), "Searching source...", true) { @Override public void run(@NotNull final ProgressIndicator indicator) { String artifactUrl = null; SourceSearcher[] searchers = {new MavenCentralSourceSearcher(), new SonatypeSourceSearcher()}; for (SourceSearcher searcher : searchers) { try { artifactUrl = searcher.findSourceJar(indicator, artifactId, version, jar); } catch (SourceSearchException e) { LOG.warn(e); showMessage("Downloading failed", e.getMessage(), NotificationType.ERROR); continue; } if (artifactUrl != null) break; } if (artifactUrl == null) { showMessage("Sources not found", "Sources for '" + jarName + ".jar' not found", NotificationType.WARNING); return; } if (!(libSourceDir.isDirectory() || libSourceDir.mkdirs())) { showMessage("Downloading failed", "Failed to create directory to store sources: " + libSourceDir, NotificationType.ERROR); return; } try { File tmpDownload = FileUtil.createTempFile(libSourceDir, "download.", ".tmp", false, false); HttpRequests.request(artifactUrl).saveToFile(tmpDownload, indicator); if (!sourceFile.exists() && !tmpDownload.renameTo(sourceFile)) { LOG.warn("Failed to rename file " + tmpDownload + " to " + sourceFileName); } } catch (IOException e) { LOG.warn(e); showMessage("Downloading failed", "Connection problem. See log for more details.", NotificationType.ERROR); } } @Override public void onSuccess() { attachSourceJar(sourceFile, libraries); } private void showMessage(String title, String message, NotificationType notificationType) { new Notification("Source searcher", title, message, notificationType).notify(getProject()); } }; task.queue(); return ActionCallback.DONE; } }); } private static boolean isRootInExistingFile(VirtualFile root) { if (root.getFileSystem() instanceof JarFileSystem) { VirtualFile jar = JarFileSystem.getInstance().getVirtualFileForJar(root); // we might be invoked outside EDT, so sync VFS refresh is impossible, so we check java.io.File existence if (jar == null || !VfsUtilCore.virtualToIoFile(jar).exists()) return false; } return true; } public static void attachSourceJar(@NotNull File sourceJar, @NotNull Collection<Library> libraries) { VirtualFile srcFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(sourceJar); if (srcFile == null) return; VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(srcFile); if (jarRoot == null) return; VirtualFile[] roots = LibrarySourceRootDetectorUtil.scanAndSelectDetectedJavaSourceRoots(null, new VirtualFile[]{jarRoot}); if (roots.length == 0) { roots = new VirtualFile[]{jarRoot}; } doAttachSourceJars(libraries, roots); } private static void doAttachSourceJars(@NotNull Collection<Library> libraries, VirtualFile[] roots) { WriteAction.run(() -> { for (Library library : libraries) { Library.ModifiableModel model = library.getModifiableModel(); Set<VirtualFile> alreadyExistingFiles = ContainerUtil.newHashSet(model.getFiles(OrderRootType.SOURCES)); for (VirtualFile root : roots) { if (!alreadyExistingFiles.contains(root)) { model.addRoot(root, OrderRootType.SOURCES); } } model.commit(); } }); } public static File getLibrarySourceDir() { String path = System.getProperty("idea.library.source.dir"); return path != null ? new File(path) : new File(SystemProperties.getUserHome(), ".ideaLibSources"); } }