4ba4b547235b1012764fae700a1e727f216c3c6f
[idea/community.git] / platform / lang-impl / src / com / intellij / webcore / packaging / InstalledPackagesPanel.java
1 package com.intellij.webcore.packaging;
2
3 import com.google.common.collect.Lists;
4 import com.intellij.icons.AllIcons;
5 import com.intellij.ide.ActivityTracker;
6 import com.intellij.openapi.actionSystem.AnActionEvent;
7 import com.intellij.openapi.application.Application;
8 import com.intellij.openapi.application.ApplicationManager;
9 import com.intellij.openapi.application.ModalityState;
10 import com.intellij.openapi.project.Project;
11 import com.intellij.openapi.projectRoots.Sdk;
12 import com.intellij.openapi.ui.Messages;
13 import com.intellij.openapi.util.text.StringUtil;
14 import com.intellij.ui.*;
15 import com.intellij.ui.table.JBTable;
16 import com.intellij.util.CatchingConsumer;
17 import com.intellij.util.Consumer;
18 import com.intellij.util.IconUtil;
19 import com.intellij.util.ObjectUtils;
20 import com.intellij.util.containers.ContainerUtil;
21 import com.intellij.util.containers.HashSet;
22 import com.intellij.util.ui.StatusText;
23 import com.intellij.util.ui.UIUtil;
24 import org.jetbrains.annotations.NotNull;
25 import org.jetbrains.annotations.Nullable;
26
27 import javax.swing.*;
28 import javax.swing.event.ListSelectionEvent;
29 import javax.swing.event.ListSelectionListener;
30 import javax.swing.table.DefaultTableCellRenderer;
31 import javax.swing.table.DefaultTableModel;
32 import javax.swing.table.TableCellRenderer;
33 import java.awt.*;
34 import java.awt.event.MouseEvent;
35 import java.io.IOException;
36 import java.util.*;
37 import java.util.List;
38 import java.util.concurrent.atomic.AtomicInteger;
39
40 public class InstalledPackagesPanel extends JPanel {
41   private final AnActionButton myUpgradeButton;
42   protected final AnActionButton myInstallButton;
43   private final AnActionButton myUninstallButton;
44
45   protected final JBTable myPackagesTable;
46   private DefaultTableModel myPackagesTableModel;
47   // can be accessed from any thread
48   protected volatile PackageManagementService myPackageManagementService;
49   protected final Project myProject;
50   protected final PackagesNotificationPanel myNotificationArea;
51   protected final List<Consumer<Sdk>> myPathChangedListeners = ContainerUtil.createLockFreeCopyOnWriteList();
52   private final Set<String> myCurrentlyInstalling = ContainerUtil.newHashSet();
53   private final Set<InstalledPackage> myWaitingToUpgrade = ContainerUtil.newHashSet();
54
55   public InstalledPackagesPanel(Project project, PackagesNotificationPanel area) {
56     super(new BorderLayout());
57     myProject = project;
58     myNotificationArea = area;
59
60     myPackagesTableModel = new DefaultTableModel(new String[]{"Package", "Version", "Latest"}, 0) {
61       @Override
62       public boolean isCellEditable(int i, int i1) {
63         return false;
64       }
65     };
66     final TableCellRenderer tableCellRenderer = new MyTableCellRenderer();
67     myPackagesTable = new JBTable(myPackagesTableModel) {
68       @Override
69       public TableCellRenderer getCellRenderer(int row, int column) {
70         return tableCellRenderer;
71       }
72     };
73     // Defence from javax.swing.JTable.initializeLocalVars:
74     //     setPreferredScrollableViewportSize(new Dimension(450, 400));
75     myPackagesTable.setPreferredScrollableViewportSize(null);
76     myPackagesTable.setStriped(true);
77     myPackagesTable.getTableHeader().setReorderingAllowed(false);
78
79     myUpgradeButton = new AnActionButton("Upgrade", IconUtil.getMoveUpIcon()) {
80       @Override
81       public void actionPerformed(@NotNull AnActionEvent e) {
82         upgradeAction();
83       }
84     };
85     myInstallButton = new AnActionButton("Install", IconUtil.getAddIcon()) {
86       @Override
87       public void actionPerformed(@NotNull AnActionEvent e) {
88         if (myPackageManagementService != null) {
89           ManagePackagesDialog dialog = createManagePackagesDialog();
90           dialog.show();
91         }
92       }
93     };
94     myUninstallButton = new AnActionButton("Uninstall", IconUtil.getRemoveIcon()) {
95       @Override
96       public void actionPerformed(@NotNull AnActionEvent e) {
97         uninstallAction();
98       }
99     };
100     ToolbarDecorator decorator =
101       ToolbarDecorator.createDecorator(myPackagesTable).disableUpDownActions().disableAddAction().disableRemoveAction()
102         .addExtraAction(myInstallButton)
103         .addExtraAction(myUninstallButton)
104         .addExtraAction(myUpgradeButton);
105
106     add(decorator.createPanel());
107     myInstallButton.setEnabled(false);
108     myUninstallButton.setEnabled(false);
109     myUpgradeButton.setEnabled(false);
110
111     myPackagesTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
112       @Override
113       public void valueChanged(ListSelectionEvent event) {
114         updateUninstallUpgrade();
115       }
116     });
117
118     new DoubleClickListener() {
119       @Override
120       protected boolean onDoubleClick(MouseEvent e) {
121         if (myPackageManagementService != null && myInstallButton.isEnabled()) {
122           ManagePackagesDialog dialog = createManagePackagesDialog();
123           Point p = e.getPoint();
124           int row = myPackagesTable.rowAtPoint(p);
125           int column = myPackagesTable.columnAtPoint(p);
126           if (row >= 0 && column >= 0) {
127             Object pkg = myPackagesTable.getValueAt(row, 0);
128             if (pkg instanceof InstalledPackage) {
129               dialog.selectPackage((InstalledPackage) pkg);
130             }
131           }
132           dialog.show();
133           return true;
134         }
135         return false;
136       }
137     }.installOn(myPackagesTable);
138   }
139
140   @NotNull
141   protected ManagePackagesDialog createManagePackagesDialog() {
142     return new ManagePackagesDialog(myProject,
143                                     myPackageManagementService,
144                                     new PackageManagementService.Listener() {
145                                       @Override
146                                       public void operationStarted(String packageName) {
147                                         myPackagesTable.setPaintBusy(true);
148                                       }
149
150                                       @Override
151                                       public void operationFinished(String packageName,
152                                                                     @Nullable PackageManagementService.ErrorDescription errorDescription) {
153                                         myNotificationArea.showResult(packageName, errorDescription);
154                                         myPackagesTable.clearSelection();
155                                         doUpdatePackages(myPackageManagementService);
156                                       }
157                                     });
158   }
159
160   public void addPathChangedListener(Consumer<Sdk> consumer) {
161     myPathChangedListeners.add(consumer);
162   }
163
164   private void upgradeAction() {
165     final int[] rows = myPackagesTable.getSelectedRows();
166     if (myPackageManagementService != null) {
167       final Set<String> upgradedPackages = new HashSet<String>();
168       final Set<String> packagesShouldBePostponed = getPackagesToPostpone();
169       for (int row : rows) {
170         final Object packageObj = myPackagesTableModel.getValueAt(row, 0);
171         if (packageObj instanceof InstalledPackage) {
172           InstalledPackage pkg = (InstalledPackage)packageObj;
173           final String packageName = pkg.getName();
174           final String currentVersion = pkg.getVersion();
175           final String availableVersion = (String)myPackagesTableModel.getValueAt(row, 2);
176
177           if (packagesShouldBePostponed.contains(packageName)) {
178             myWaitingToUpgrade.add((InstalledPackage)packageObj);
179           }
180           else if (isUpdateAvailable(currentVersion, availableVersion)) {
181             upgradePackage(pkg, availableVersion);
182             upgradedPackages.add(packageName);
183           }
184         }
185       }
186
187       if (myCurrentlyInstalling.isEmpty() && upgradedPackages.isEmpty() && !myWaitingToUpgrade.isEmpty()) {
188         upgradePostponedPackages();
189       }
190     }
191   }
192
193   private void upgradePostponedPackages() {
194     final Iterator<InstalledPackage> iterator = myWaitingToUpgrade.iterator();
195     final InstalledPackage toUpgrade = iterator.next();
196     iterator.remove();
197     upgradePackage(toUpgrade, toUpgrade.getVersion());
198   }
199
200   protected Set<String> getPackagesToPostpone() {
201     return Collections.emptySet();
202   }
203
204   private void upgradePackage(@NotNull final InstalledPackage pkg, @Nullable final String toVersion) {
205     final PackageManagementService selPackageManagementService = myPackageManagementService;
206     myPackageManagementService.fetchPackageVersions(pkg.getName(), new CatchingConsumer<List<String>, Exception>() {
207       @Override
208       public void consume(List<String> releases) {
209         if (!releases.isEmpty() && !isUpdateAvailable(pkg.getVersion(), releases.get(0))) {
210           return;
211         }
212
213         ApplicationManager.getApplication().invokeLater(new Runnable() {
214           @Override
215           public void run() {
216             final PackageManagementService.Listener listener = new PackageManagementService.Listener() {
217               @Override
218               public void operationStarted(final String packageName) {
219                 UIUtil.invokeLaterIfNeeded(new Runnable() {
220                   @Override
221                   public void run() {
222                     myPackagesTable.setPaintBusy(true);
223                     myCurrentlyInstalling.add(packageName);
224                   }
225                 });
226               }
227
228               @Override
229               public void operationFinished(final String packageName,
230                                             @Nullable final PackageManagementService.ErrorDescription errorDescription) {
231                 UIUtil.invokeLaterIfNeeded(new Runnable() {
232                   @Override
233                   public void run() {
234                     myPackagesTable.clearSelection();
235                     updatePackages(selPackageManagementService);
236                     myPackagesTable.setPaintBusy(false);
237                     myCurrentlyInstalling.remove(packageName);
238                     if (errorDescription == null) {
239                       myNotificationArea.showSuccess("Package " + packageName + " successfully upgraded");
240                     }
241                     else {
242                       myNotificationArea.showError("Upgrade packages failed. <a href=\"xxx\">Details...</a>", "Upgrade Packages Failed",
243                                                    errorDescription);
244                     }
245
246                     if (myCurrentlyInstalling.isEmpty() && !myWaitingToUpgrade.isEmpty()) {
247                       upgradePostponedPackages();
248                     }
249                   }
250                 });
251               }
252             };
253             PackageManagementServiceEx serviceEx = getServiceEx();
254             if (serviceEx != null) {
255               serviceEx.updatePackage(pkg, toVersion, listener);
256             }
257             else {
258               myPackageManagementService.installPackage(new RepoPackage(pkg.getName(), null /* TODO? */), null, true, null, listener, false);
259             }
260             myUpgradeButton.setEnabled(false);
261           }
262         }, ModalityState.any());
263       }
264
265       @Override
266       public void consume(Exception e) {
267         ApplicationManager.getApplication().invokeLater(new Runnable() {
268           @Override
269           public void run() {
270             Messages.showErrorDialog("Error occurred. Please, check your internet connection.",
271                                      "Upgrade Package Failed.");
272           }
273         }, ModalityState.any());
274       }
275     });
276   }
277
278   @Nullable
279   private PackageManagementServiceEx getServiceEx() {
280     return ObjectUtils.tryCast(myPackageManagementService, PackageManagementServiceEx.class);
281   }
282
283   private void updateUninstallUpgrade() {
284     final int[] selected = myPackagesTable.getSelectedRows();
285     boolean upgradeAvailable = false;
286     boolean canUninstall = selected.length != 0;
287     boolean canInstall = true;
288     boolean canUpgrade = true;
289     if (myPackageManagementService != null && selected.length != 0) {
290       for (int i = 0; i != selected.length; ++i) {
291         final int index = selected[i];
292         if (index >= myPackagesTable.getRowCount()) continue;
293         final Object value = myPackagesTable.getValueAt(index, 0);
294         if (value instanceof InstalledPackage) {
295           final InstalledPackage pkg = (InstalledPackage)value;
296           if (!canUninstallPackage(pkg)) {
297             canUninstall = false;
298           }
299           canInstall = canInstallPackage(pkg);
300           if (!canUpgradePackage(pkg)) {
301             canUpgrade = false;
302           }
303           final String pyPackageName = pkg.getName();
304           final String availableVersion = (String)myPackagesTable.getValueAt(index, 2);
305           if (!upgradeAvailable) {
306             upgradeAvailable = isUpdateAvailable(pkg.getVersion(), availableVersion) &&
307                                !myCurrentlyInstalling.contains(pyPackageName);
308           }
309           if (!canUninstall && !canUpgrade) break;
310         }
311       }
312     }
313     myUninstallButton.setEnabled(canUninstall);
314     myInstallButton.setEnabled(canInstall);
315     myUpgradeButton.setEnabled(upgradeAvailable && canUpgrade);
316   }
317
318   protected boolean canUninstallPackage(InstalledPackage pyPackage) {
319     return true;
320   }
321
322   protected boolean canInstallPackage(@NotNull final InstalledPackage pyPackage) {
323     return true;
324   }
325
326   protected boolean canUpgradePackage(InstalledPackage pyPackage) {
327     return true;
328   }
329
330   private void uninstallAction() {
331     final List<InstalledPackage> packages = getSelectedPackages();
332     final PackageManagementService selPackageManagementService = myPackageManagementService;
333     if (selPackageManagementService != null) {
334       PackageManagementService.Listener listener = new PackageManagementService.Listener() {
335         @Override
336         public void operationStarted(String packageName) {
337           UIUtil.invokeLaterIfNeeded(new Runnable() {
338             @Override
339             public void run() {
340               myPackagesTable.setPaintBusy(true);
341             }
342           });
343         }
344
345         @Override
346         public void operationFinished(final String packageName,
347                                       @Nullable final PackageManagementService.ErrorDescription errorDescription) {
348           UIUtil.invokeLaterIfNeeded(new Runnable() {
349             @Override
350             public void run() {
351               myPackagesTable.clearSelection();
352               updatePackages(selPackageManagementService);
353               myPackagesTable.setPaintBusy(false);
354               if (errorDescription == null) {
355                 if (packageName != null) {
356                   myNotificationArea.showSuccess("Package '" + packageName + "' successfully uninstalled");
357                 }
358                 else {
359                   myNotificationArea.showSuccess("Packages successfully uninstalled");
360                 }
361               }
362               else {
363                 myNotificationArea.showError("Uninstall packages failed. <a href=\"xxx\">Details...</a>", "Uninstall Packages Failed",
364                                              errorDescription);
365               }
366             }
367           });
368         }
369       };
370       myPackageManagementService.uninstallPackages(packages, listener);
371     }
372   }
373
374   @NotNull
375   private List<InstalledPackage> getSelectedPackages() {
376     final List<InstalledPackage> results = new ArrayList<InstalledPackage>();
377     final int[] rows = myPackagesTable.getSelectedRows();
378     for (int row : rows) {
379       final Object packageName = myPackagesTableModel.getValueAt(row, 0);
380       if (packageName instanceof InstalledPackage) {
381         results.add((InstalledPackage)packageName);
382       }
383     }
384     return results;
385   }
386
387   public void updatePackages(@Nullable PackageManagementService packageManagementService) {
388     myPackageManagementService = packageManagementService;
389     myPackagesTable.clearSelection();
390     myPackagesTableModel.getDataVector().clear();
391     myPackagesTableModel.fireTableDataChanged();
392     if (packageManagementService != null) {
393       doUpdatePackages(packageManagementService);
394     }
395   }
396
397   private void onUpdateStarted() {
398     myPackagesTable.setPaintBusy(true);
399     myPackagesTable.getEmptyText().setText("Loading...");
400   }
401
402   private void onUpdateFinished() {
403     myPackagesTable.setPaintBusy(false);
404     myPackagesTable.getEmptyText().setText(StatusText.DEFAULT_EMPTY_TEXT);
405     updateUninstallUpgrade();
406     // Action button presentations won't be updated if no events occur (e.g. mouse isn't moving, keys aren't being pressed).
407     // In that case emulating activity will help:
408     ActivityTracker.getInstance().inc();
409   }
410
411   public void doUpdatePackages(@NotNull final PackageManagementService packageManagementService) {
412     onUpdateStarted();
413     final Application application = ApplicationManager.getApplication();
414     application.executeOnPooledThread(new Runnable() {
415       @Override
416       public void run() {
417         Collection<InstalledPackage> packages = Lists.newArrayList();
418         try {
419           packages = packageManagementService.getInstalledPackages();
420         }
421         catch (IOException e) {
422           // do nothing, we already have an empty list
423         }
424         finally {
425           final Collection<InstalledPackage> finalPackages = packages;
426
427           final Map<String, RepoPackage> cache = buildNameToPackageMap(packageManagementService.getAllPackagesCached());
428           final boolean shouldFetchLatestVersionsForOnlyInstalledPackages = shouldFetchLatestVersionsForOnlyInstalledPackages();
429           if (cache.isEmpty()) {
430             if (!shouldFetchLatestVersionsForOnlyInstalledPackages) {
431               refreshLatestVersions(packageManagementService);
432             }
433           }
434           UIUtil.invokeLaterIfNeeded(new Runnable() {
435             @Override
436             public void run() {
437               if (packageManagementService == myPackageManagementService) {
438                 myPackagesTableModel.getDataVector().clear();
439                 for (InstalledPackage pkg : finalPackages) {
440                   RepoPackage repoPackage = cache.get(pkg.getName());
441                   final String version = repoPackage != null ? repoPackage.getLatestVersion() : null;
442                   myPackagesTableModel
443                     .addRow(new Object[]{pkg, pkg.getVersion(), version == null ? "" : version});
444                 }
445                 if (!cache.isEmpty()) {
446                   onUpdateFinished();
447                 }
448                 if (shouldFetchLatestVersionsForOnlyInstalledPackages) {
449                   setLatestVersionsForInstalledPackages();
450                 }
451               }
452             }
453           });
454         }
455       }
456     });
457   }
458
459   private InstalledPackage getInstalledPackageAt(int index) {
460     return (InstalledPackage) myPackagesTableModel.getValueAt(index, 0);
461   }
462
463   private void setLatestVersionsForInstalledPackages() {
464     final PackageManagementServiceEx serviceEx = getServiceEx();
465     if (serviceEx == null) {
466       return;
467     }
468     int packageCount = myPackagesTableModel.getRowCount();
469     if (packageCount == 0) {
470       onUpdateFinished();
471     }
472     final AtomicInteger inProgressPackageCount = new AtomicInteger(packageCount);
473     for (int i = 0; i < packageCount; ++i) {
474       final int finalIndex = i;
475       final InstalledPackage pkg = getInstalledPackageAt(finalIndex);
476       serviceEx.fetchLatestVersion(pkg, new CatchingConsumer<String, Exception>() {
477
478         private void decrement() {
479           if (inProgressPackageCount.decrementAndGet() == 0) {
480             onUpdateFinished();
481           }
482         }
483
484         @Override
485         public void consume(Exception e) {
486           UIUtil.invokeLaterIfNeeded(new Runnable() {
487             @Override
488             public void run() {
489               decrement();
490             }
491           });
492         }
493
494         @Override
495         public void consume(@Nullable final String latestVersion) {
496           UIUtil.invokeLaterIfNeeded(new Runnable() {
497             @Override
498             public void run() {
499               if (finalIndex < myPackagesTableModel.getRowCount()) {
500                 InstalledPackage p = getInstalledPackageAt(finalIndex);
501                 if (pkg == p) {
502                   myPackagesTableModel.setValueAt(latestVersion, finalIndex, 2);
503                 }
504               }
505               decrement();
506             }
507           });
508         }
509       });
510     }
511   }
512
513   private boolean shouldFetchLatestVersionsForOnlyInstalledPackages() {
514     PackageManagementServiceEx serviceEx = getServiceEx();
515     if (serviceEx != null) {
516       return serviceEx.shouldFetchLatestVersionsForOnlyInstalledPackages();
517     }
518     return false;
519   }
520
521   private boolean isUpdateAvailable(@NotNull String currentVersion, @Nullable String availableVersion) {
522     if (availableVersion == null) {
523       return false;
524     }
525     PackageManagementService service = myPackageManagementService;
526     if (service != null) {
527       return service.compareVersions(currentVersion, availableVersion) < 0;
528     }
529     return PackageVersionComparator.VERSION_COMPARATOR.compare(currentVersion, availableVersion) < 0;
530   }
531
532   private void refreshLatestVersions(@NotNull final PackageManagementService packageManagementService) {
533     final Application application = ApplicationManager.getApplication();
534     application.executeOnPooledThread(new Runnable() {
535       @Override
536       public void run() {
537         if (packageManagementService == myPackageManagementService) {
538           try {
539             List<RepoPackage> packages = packageManagementService.reloadAllPackages();
540             final Map<String, RepoPackage> packageMap = buildNameToPackageMap(packages);
541             application.invokeLater(new Runnable() {
542               @Override
543               public void run() {
544                 for (int i = 0; i != myPackagesTableModel.getRowCount(); ++i) {
545                   final InstalledPackage pyPackage = (InstalledPackage)myPackagesTableModel.getValueAt(i, 0);
546                   final RepoPackage repoPackage = packageMap.get(pyPackage.getName());
547                   myPackagesTableModel.setValueAt(repoPackage == null ? null : repoPackage.getLatestVersion(), i, 2);
548                 }
549                 myPackagesTable.setPaintBusy(false);
550               }
551             }, ModalityState.stateForComponent(myPackagesTable));
552           }
553           catch (IOException ignored) {
554             myPackagesTable.setPaintBusy(false);
555           }
556         }
557       }
558     });
559   }
560
561   private static Map<String, RepoPackage> buildNameToPackageMap(List<RepoPackage> packages) {
562     final Map<String, RepoPackage> packageMap = new HashMap<String, RepoPackage>();
563     for (RepoPackage aPackage : packages) {
564       packageMap.put(aPackage.getName(), aPackage);
565     }
566     return packageMap;
567   }
568
569   private class MyTableCellRenderer extends DefaultTableCellRenderer {
570     @Override
571     public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected,
572                                                    final boolean hasFocus, final int row, final int column) {
573       final JLabel cell = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
574       final String version = (String)table.getValueAt(row, 1);
575       final String availableVersion = (String)table.getValueAt(row, 2);
576       boolean update = column == 2 &&
577                        StringUtil.isNotEmpty(availableVersion) &&
578                        isUpdateAvailable(version, availableVersion);
579       cell.setIcon(update ? AllIcons.Vcs.Arrow_right : null);
580       final Object pyPackage = table.getValueAt(row, 0);
581       if (pyPackage instanceof InstalledPackage) {
582         cell.setToolTipText(((InstalledPackage) pyPackage).getTooltipText());
583       }
584       return cell;
585     }
586   }
587 }