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