/* * 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 org.jetbrains.plugins.groovy.grape; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.execution.CantRunException; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.configurations.JavaParameters; import com.intellij.execution.process.OSProcessHandler; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.notification.NotificationDisplayType; import com.intellij.notification.NotificationGroup; import com.intellij.notification.NotificationType; import com.intellij.openapi.application.Result; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.JavaSdkType; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ModifiableRootModel; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.OrderRootType; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.libraries.LibraryTable; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.JarFileSystem; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.search.LocalSearchScope; import com.intellij.psi.search.searches.ReferencesSearch; import com.intellij.psi.util.CachedValueProvider; import com.intellij.psi.util.CachedValuesManager; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ExceptionUtil; import com.intellij.util.Function; import com.intellij.util.IncorrectOperationException; import com.intellij.util.PathUtil; import com.intellij.util.containers.ContainerUtil; import gnu.trove.THashSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.groovy.lang.psi.GrReferenceElement; import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation; import org.jetbrains.plugins.groovy.lang.psi.api.types.GrCodeReferenceElement; import org.jetbrains.plugins.groovy.runner.DefaultGroovyScriptRunner; import org.jetbrains.plugins.groovy.runner.GroovyScriptRunConfiguration; import java.io.File; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.*; /** * @author peter */ public class GrabDependencies implements IntentionAction { private static final Logger LOG = Logger.getInstance("#org.jetbrains.plugins.groovy.grape.GrabDependencies"); private static final NotificationGroup NOTIFICATION_GROUP = new NotificationGroup("Grape", NotificationDisplayType.BALLOON, true); public static final String GRAPE_RUNNER = "org.jetbrains.plugins.groovy.grape.GrapeRunner"; @Override @NotNull public String getText() { return "Grab the artifacts"; } @Override @NotNull public String getFamilyName() { return "Grab"; } @Override public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { if (!isCorrectModule(file)) return false; int offset = editor.getCaretModel().getOffset(); final GrAnnotation anno = PsiTreeUtil.findElementOfClassAtOffset(file, offset, GrAnnotation.class, false); if (anno != null && isGrabAnnotation(anno)) { return true; } PsiElement at = file.findElementAt(offset); if (at != null && isUnresolvedRefName(at) && findGrab(file) != null) { return true; } return false; } private static PsiAnnotation findGrab(final PsiFile file) { if (!(file instanceof GroovyFile)) return null; return CachedValuesManager.getCachedValue(file, () -> { PsiClass grab = JavaPsiFacade.getInstance(file.getProject()).findClass(GrabAnnos.GRAB_ANNO, file.getResolveScope()); final Ref<PsiAnnotation> result = Ref.create(); if (grab != null) { ReferencesSearch.search(grab, new LocalSearchScope(file)).forEach(reference -> { if (reference instanceof GrCodeReferenceElement) { PsiElement parent = ((GrCodeReferenceElement)reference).getParent(); if (parent instanceof PsiAnnotation) { result.set((PsiAnnotation)parent); return false; } } return true; }); } return CachedValueProvider.Result.create(result.get(), file); }); } private static boolean isUnresolvedRefName(@NotNull PsiElement at) { PsiElement parent = at.getParent(); return parent instanceof GrReferenceElement && ((GrReferenceElement)parent).getReferenceNameElement() == at && ((GrReferenceElement)parent).resolve() == null; } private static boolean isGrabAnnotation(@NotNull GrAnnotation anno) { final String qname = anno.getQualifiedName(); return qname != null && (qname.startsWith(GrabAnnos.GRAB_ANNO) || GrabAnnos.GRAPES_ANNO.equals(qname)); } private static boolean isCorrectModule(PsiFile file) { final Module module = ModuleUtilCore.findModuleForPsiElement(file); if (module == null) { return false; } final Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); if (sdk == null) { return false; } return file.getOriginalFile().getVirtualFile() != null && sdk.getSdkType() instanceof JavaSdkType; } @Override public void invoke(@NotNull final Project project, Editor editor, PsiFile file) throws IncorrectOperationException { final Module module = ModuleUtilCore.findModuleForPsiElement(file); assert module != null; final VirtualFile vfile = file.getOriginalFile().getVirtualFile(); assert vfile != null; if (JavaPsiFacade.getInstance(project).findClass("org.apache.ivy.core.report.ResolveReport", file.getResolveScope()) == null) { Messages.showErrorDialog("Sorry, but IDEA cannot @Grab the dependencies without Ivy. Please add Ivy to your module dependencies and re-run the action.", "Ivy Missing"); return; } Map<String, String> queries = prepareQueries(file); final Map<String, GeneralCommandLine> lines = new HashMap<>(); for (String grabText : queries.keySet()) { final JavaParameters javaParameters = GroovyScriptRunConfiguration.createJavaParametersWithSdk(module); //debug //javaParameters.getVMParametersList().add("-Xdebug"); //javaParameters.getVMParametersList().add("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5239"); try { DefaultGroovyScriptRunner.configureGenericGroovyRunner(javaParameters, module, GRAPE_RUNNER, false, true); javaParameters.getClassPath().add(PathUtil.getJarPathForClass(GrapeRunner.class)); javaParameters.getProgramParametersList().add(queries.get(grabText)); javaParameters.setUseDynamicClasspath(true); lines.put(grabText, javaParameters.toCommandLine()); } catch (CantRunException e) { String title = "Can't run @Grab: " + ExceptionUtil.getMessage(e); NOTIFICATION_GROUP.createNotification(title, ExceptionUtil.getThrowableText(e), NotificationType.ERROR, null).notify(project); return; } } ProgressManager.getInstance().run(new Task.Backgroundable(project, "Processing @Grab Annotations") { @Override public void run(@NotNull ProgressIndicator indicator) { int jarCount = 0; String messages = ""; for (Map.Entry<String, GeneralCommandLine> entry : lines.entrySet()) { String grabText = entry.getKey(); indicator.setText2(grabText); try { final GrapeProcessHandler handler = new GrapeProcessHandler(entry.getValue(), module); handler.startNotify(); handler.waitFor(); jarCount += handler.jarCount; messages += "<b>" + grabText + "</b>: " + handler.messages + "<p>"; } catch (ExecutionException e) { LOG.error(e); } } final String finalMessages = messages; final String title = jarCount + " Grape dependency jar" + (jarCount == 1 ? "" : "s") + " added"; NOTIFICATION_GROUP.createNotification(title, finalMessages, NotificationType.INFORMATION, null).notify(project); } }); } static Map<String, String> prepareQueries(PsiFile file) { final Set<GrAnnotation> grabs = new LinkedHashSet<>(); final Set<GrAnnotation> excludes = new THashSet<>(); final Set<GrAnnotation> resolvers = new THashSet<>(); file.acceptChildren(new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement element) { if (element instanceof GrAnnotation) { GrAnnotation anno = (GrAnnotation)element; String qname = anno.getQualifiedName(); if (GrabAnnos.GRAB_ANNO.equals(qname)) grabs.add(anno); else if (GrabAnnos.GRAB_EXCLUDE_ANNO.equals(qname)) excludes.add(anno); else if (GrabAnnos.GRAB_RESOLVER_ANNO.equals(qname)) resolvers.add(anno); } super.visitElement(element); } }); Function<GrAnnotation, String> mapper = grAnnotation -> grAnnotation.getText(); String common = StringUtil.join(excludes, mapper, " ") + " " + StringUtil.join(resolvers, mapper, " "); LinkedHashMap<String, String> result = new LinkedHashMap<>(); for (GrAnnotation grab : grabs) { String grabText = grab.getText(); result.put(grabText, (grabText + " " + common).trim()); } return result; } @Override public boolean startInWriteAction() { return false; } private static class GrapeProcessHandler extends OSProcessHandler { private final StringBuilder myStdOut = new StringBuilder(); private final StringBuilder myStdErr = new StringBuilder(); private final Module myModule; public GrapeProcessHandler(GeneralCommandLine commandLine, Module module) throws ExecutionException { super(commandLine); myModule = module; } @Override public void notifyTextAvailable(String text, Key outputType) { text = StringUtil.convertLineSeparators(text); if (LOG.isDebugEnabled()) { LOG.debug(outputType + text); } if (outputType == ProcessOutputTypes.STDOUT) { myStdOut.append(text); } else if (outputType == ProcessOutputTypes.STDERR) { myStdErr.append(text); } } private void addGrapeDependencies(List<VirtualFile> jars) { final ModifiableRootModel model = ModuleRootManager.getInstance(myModule).getModifiableModel(); final LibraryTable.ModifiableModel tableModel = model.getModuleLibraryTable().getModifiableModel(); for (VirtualFile jar : jars) { final VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(jar); if (jarRoot != null) { OrderRootType rootType = OrderRootType.CLASSES; String libName = "Grab:" + jar.getName(); for (String classifier : ContainerUtil.ar("sources", "source", "src")) { if (libName.endsWith("-" + classifier + ".jar")) { rootType = OrderRootType.SOURCES; libName = StringUtil.trimEnd(libName, "-" + classifier + ".jar") + ".jar"; } } Library library = tableModel.getLibraryByName(libName); if (library == null) { library = tableModel.createLibrary(libName); } final Library.ModifiableModel libModel = library.getModifiableModel(); for (String url : libModel.getUrls(rootType)) { libModel.removeRoot(url, rootType); } libModel.addRoot(jarRoot, rootType); libModel.commit(); } } tableModel.commit(); model.commit(); } int jarCount; String messages = ""; @Override protected void notifyProcessTerminated(int exitCode) { try { final List<VirtualFile> jars = new ArrayList<>(); for (String line : myStdOut.toString().split("\n")) { if (line.startsWith(GrapeRunner.URL_PREFIX)) { try { final URL url = new URL(line.substring(GrapeRunner.URL_PREFIX.length())); final File libFile = new File(url.toURI()); if (libFile.exists() && libFile.getName().endsWith(".jar")) { ContainerUtil.addIfNotNull(jars, LocalFileSystem.getInstance().refreshAndFindFileByIoFile(libFile)); } } catch (MalformedURLException | URISyntaxException e) { LOG.error(e); } } } new WriteAction() { @Override protected void run(@NotNull Result result) throws Throwable { jarCount = jars.size(); messages = jarCount + " jar"; if (jarCount != 1) { messages += "s"; } if (jarCount == 0) { messages += "<br>" + myStdOut.toString().replaceAll("\n", "<br>") + "<p>" + myStdErr.toString().replaceAll("\n", "<br>"); } if (!jars.isEmpty()) { addGrapeDependencies(jars); } } }.execute(); } finally { super.notifyProcessTerminated(exitCode); } } } }