/* * Copyright 2000-2014 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.ide.plugins; import com.intellij.CommonBundle; import com.intellij.icons.AllIcons; import com.intellij.ide.BrowserUtil; import com.intellij.ide.DataManager; import com.intellij.ide.IdeBundle; import com.intellij.ide.plugins.sorters.SortByStatusAction; import com.intellij.ide.ui.search.SearchUtil; import com.intellij.ide.ui.search.SearchableOptionsRegistrar; import com.intellij.notification.*; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationNamesInfo; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.application.ex.ApplicationInfoEx; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.updateSettings.impl.PluginDownloader; import com.intellij.openapi.updateSettings.impl.UpdateChecker; import com.intellij.openapi.updateSettings.impl.UpdateSettings; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.*; import com.intellij.ui.border.CustomLineBorder; import com.intellij.ui.components.JBLabel; import com.intellij.ui.speedSearch.SpeedSearchSupply; import com.intellij.util.concurrency.SwingWorker; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.UiNotifyConnector; import com.intellij.xml.util.XmlStringUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import javax.swing.event.*; import javax.swing.plaf.BorderUIResource; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.HTMLFrameHyperlinkEvent; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.awt.*; import java.awt.event.MouseEvent; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; import java.util.*; import java.util.List; import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces; /** * @author stathik * @author Konstantin Bulenkov */ public abstract class PluginManagerMain implements Disposable { public static final String JETBRAINS_VENDOR = "JetBrains"; public static Logger LOG = Logger.getInstance("#com.intellij.ide.plugins.PluginManagerMain"); @NonNls private static final String TEXT_PREFIX = "" + " " + ""; @NonNls private static final String TEXT_SUFFIX = ""; @NonNls private static final String HTML_PREFIX = " getDependentList(IdeaPluginDescriptorImpl pluginDescriptor) { return pluginsModel.dependent(pluginDescriptor); } protected void modifyPluginsList(List list) { IdeaPluginDescriptor[] selected = pluginTable.getSelectedObjects(); pluginsModel.updatePluginsList(list); pluginsModel.filter(myFilter.getFilter().toLowerCase()); if (selected != null) { select(selected); } } protected abstract ActionGroup getActionGroup(boolean inToolbar); protected abstract PluginManagerMain getAvailable(); protected abstract PluginManagerMain getInstalled(); public JPanel getMainPanel() { return main; } protected boolean acceptHost(String host) { return true; } /** * Start a new thread which downloads new list of plugins from the site in * the background and updates a list of plugins in the table. */ protected void loadPluginsFromHostInBackground() { setDownloadStatus(true); new SwingWorker() { List list = null; List errorMessages = new ArrayList(); public Object construct() { try { list = RepositoryHelper.loadPluginsFromRepository(null); } catch (Exception e) { LOG.info(e); errorMessages.add(e.getMessage()); } String builtinPluginsUrl = ApplicationInfoEx.getInstanceEx().getBuiltinPluginsUrl(); if (builtinPluginsUrl != null) { processPluginHost(builtinPluginsUrl, true); } for (String host : UpdateSettings.getInstance().myPluginHosts) { processPluginHost(host, false); } return list; } void processPluginHost(@NotNull String host, boolean builtIn) { if (!acceptHost(host)) return; final Map downloaded = new HashMap(); try { UpdateChecker.checkPluginsHost(host, downloaded, false, null); for (PluginDownloader downloader : downloaded.values()) { final PluginNode pluginNode = PluginDownloader.createPluginNode(host, downloader); if (pluginNode != null) { if (list == null) list = new ArrayList(); list.add(pluginNode); } } } catch (ProcessCanceledException ignore) { } catch (FileNotFoundException e) { LOG.info(e); } catch (Exception e) { if (builtIn) { LOG.info("built-in repo failed: " + e.toString()); } else { LOG.info(e); errorMessages.add(e.getMessage()); } } } public void finished() { UIUtil.invokeLaterIfNeeded(new Runnable() { public void run() { setDownloadStatus(false); if (list != null) { modifyPluginsList(list); propagateUpdates(list); } if (!errorMessages.isEmpty()) { if (Messages.OK == Messages.showOkCancelDialog( IdeBundle.message("error.list.of.plugins.was.not.loaded", StringUtil.join(errorMessages, ", ")), IdeBundle.message("title.plugins"), CommonBundle.message("button.retry"), CommonBundle.getCancelButtonText(), Messages.getErrorIcon())) { loadPluginsFromHostInBackground(); } } } }); } }.start(); } protected abstract void propagateUpdates(List list); protected void setDownloadStatus(boolean status) { pluginTable.setPaintBusy(status); myBusy = status; } protected void loadAvailablePlugins() { ArrayList list; try { // If we already have a file with downloaded plugins from the last time, // then read it, load into the list and start the updating process. // Otherwise just start the process of loading the list and save it // into the persistent config file for later reading. File file = new File(PathManager.getPluginsPath(), RepositoryHelper.PLUGIN_LIST_FILE); if (file.exists()) { RepositoryContentHandler handler = new RepositoryContentHandler(); SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); parser.parse(file, handler); list = handler.getPluginsList(); modifyPluginsList(list); } } catch (Exception ex) { // Nothing to do, just ignore - if nothing can be read from the local // file just start downloading of plugins' list from the site. } loadPluginsFromHostInBackground(); } public static boolean downloadPlugins(final List plugins, final List allPlugins, final Runnable onSuccess, @Nullable final Runnable cleanup) throws IOException { final boolean[] result = new boolean[1]; try { ProgressManager.getInstance().run(new Task.Backgroundable(null, IdeBundle.message("progress.download.plugins"), true, PluginManagerUISettings.getInstance()) { @Override public void run(@NotNull ProgressIndicator indicator) { try { if (PluginInstaller.prepareToInstall(plugins, allPlugins)) { ApplicationManager.getApplication().invokeLater(onSuccess); result[0] = true; } } finally { if (cleanup != null) cleanup.run(); } } }); } catch (RuntimeException e) { if (e.getCause() != null && e.getCause() instanceof IOException) { throw (IOException)e.getCause(); } else { throw e; } } return result[0]; } public boolean isRequireShutdown() { return requireShutdown; } public void ignoreChanges() { requireShutdown = false; } public static void pluginInfoUpdate(IdeaPluginDescriptor plugin, @Nullable String filter, @NotNull JEditorPane descriptionTextArea, @NotNull PluginHeaderPanel header, PluginManagerMain manager) { if (plugin == null) { setTextValue(null, filter, descriptionTextArea); header.getPanel().setVisible(false); return; } StringBuilder sb = new StringBuilder(); header.setPlugin(plugin); String description = plugin.getDescription(); if (!isEmptyOrSpaces(description)) { sb.append(description); } String changeNotes = plugin.getChangeNotes(); if (!isEmptyOrSpaces(changeNotes)) { sb.append("

Change Notes

"); sb.append(changeNotes); } if (!plugin.isBundled()) { String vendor = plugin.getVendor(); String vendorEmail = plugin.getVendorEmail(); String vendorUrl = plugin.getVendorUrl(); if (!isEmptyOrSpaces(vendor) || !isEmptyOrSpaces(vendorEmail) || !isEmptyOrSpaces(vendorUrl)) { sb.append("

Vendor

"); if (!isEmptyOrSpaces(vendor)) { sb.append(vendor); } if (!isEmptyOrSpaces(vendorUrl)) { sb.append("
").append(composeHref(vendorUrl)); } if (!isEmptyOrSpaces(vendorEmail)) { sb.append("
") .append(HTML_PREFIX) .append("mailto:").append(vendorEmail) .append("\">").append(vendorEmail).append(HTML_SUFFIX); } } String pluginDescriptorUrl = plugin.getUrl(); if (!isEmptyOrSpaces(pluginDescriptorUrl)) { sb.append("

Plugin homepage

").append(composeHref(pluginDescriptorUrl)); } String size = plugin instanceof PluginNode ? ((PluginNode)plugin).getSize() : null; if (!isEmptyOrSpaces(size)) { sb.append("

Size

").append(PluginManagerColumnInfo.getFormattedSize(size)); } } setTextValue(sb, filter, descriptionTextArea); } private static void setTextValue(@Nullable StringBuilder text, @Nullable String filter, JEditorPane pane) { if (text != null) { text.insert(0, TEXT_PREFIX); text.append(TEXT_SUFFIX); pane.setText(SearchUtil.markup(text.toString(), filter).trim()); pane.setCaretPosition(0); } else { pane.setText(TEXT_PREFIX + TEXT_SUFFIX); } } private static String composeHref(String vendorUrl) { return HTML_PREFIX + vendorUrl + "\">" + vendorUrl + HTML_SUFFIX; } public boolean isModified() { if (requireShutdown) return true; return false; } public String apply() { final String applyMessage = canApply(); if (applyMessage != null) return applyMessage; setRequireShutdown(true); return null; } @Nullable protected String canApply() { return null; } private void createUIComponents() { myHeader = new JPanel(new BorderLayout()) { @Override public Color getBackground() { return UIUtil.getTextFieldBackground(); } }; } public static class MyHyperlinkListener implements HyperlinkListener { public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { JEditorPane pane = (JEditorPane)e.getSource(); if (e instanceof HTMLFrameHyperlinkEvent) { HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent)e; HTMLDocument doc = (HTMLDocument)pane.getDocument(); doc.processHTMLFrameHyperlinkEvent(evt); } else { URL url = e.getURL(); if (url != null) { BrowserUtil.browse(url); } } } } } private static class MySpeedSearchBar extends SpeedSearchBase { public MySpeedSearchBar(PluginTable cmp) { super(cmp); } @Override protected int convertIndexToModel(int viewIndex) { return getComponent().convertRowIndexToModel(viewIndex); } public int getSelectedIndex() { return myComponent.getSelectedRow(); } public Object[] getAllElements() { return myComponent.getElements(); } public String getElementText(Object element) { return ((IdeaPluginDescriptor)element).getName(); } public void selectElement(Object element, String selectedText) { for (int i = 0; i < myComponent.getRowCount(); i++) { if (myComponent.getObjectAt(i).getName().equals(((IdeaPluginDescriptor)element).getName())) { myComponent.setRowSelectionInterval(i, i); TableUtil.scrollSelectionToVisible(myComponent); break; } } } } public void select(IdeaPluginDescriptor... descriptors) { pluginTable.select(descriptors); } protected static boolean isAccepted(String filter, Set search, IdeaPluginDescriptor descriptor) { if (StringUtil.isEmpty(filter)) return true; if (isAccepted(search, filter, descriptor.getName())) { return true; } else { final String description = descriptor.getDescription(); if (description != null && isAccepted(search, filter, description)) { return true; } final String category = descriptor.getCategory(); if (category != null && isAccepted(search, filter, category)) { return true; } final String changeNotes = descriptor.getChangeNotes(); if (changeNotes != null && isAccepted(search, filter, changeNotes)) { return true; } } return false; } private static boolean isAccepted(final Set search, @NotNull final String filter, @NotNull final String description) { if (StringUtil.containsIgnoreCase(description, filter)) return true; final SearchableOptionsRegistrar optionsRegistrar = SearchableOptionsRegistrar.getInstance(); final HashSet descriptionSet = new HashSet(search); descriptionSet.removeAll(optionsRegistrar.getProcessedWords(description)); if (descriptionSet.isEmpty()) { return true; } return false; } public static void notifyPluginsWereInstalled(@Nullable String pluginName, final Project project) { notifyPluginsWereUpdated(pluginName != null ? "Plugin \'" + pluginName + "\' was successfully installed" : "Plugins were installed", project); } public static void notifyPluginsWereUpdated(final String title, @Nullable final Project project) { final ApplicationEx app = ApplicationManagerEx.getApplicationEx(); final boolean restartCapable = app.isRestartCapable(); String message = restartCapable ? IdeBundle.message("message.idea.restart.required", ApplicationNamesInfo.getInstance().getFullProductName()) : IdeBundle.message("message.idea.shutdown.required", ApplicationNamesInfo.getInstance().getFullProductName()); message += "
Restart now" : "\"shutdown\">Shutdown"; message += ""; new NotificationGroup("Plugins Lifecycle Group", NotificationDisplayType.STICKY_BALLOON, true) .createNotification(title, XmlStringUtil.wrapInHtml(message), NotificationType.INFORMATION, new NotificationListener() { @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { notification.expire(); if (restartCapable) { app.restart(true); } else { app.exit(false, true); } } }).notify(project); } public class MyPluginsFilter extends FilterComponent { public MyPluginsFilter() { super("PLUGIN_FILTER", 5); } public void filter() { getPluginTable().putClientProperty(SpeedSearchSupply.SEARCH_QUERY_KEY, getFilter()); pluginsModel.filter(getFilter().toLowerCase()); TableUtil.ensureSelectionExists(getPluginTable()); } } protected class RefreshAction extends DumbAwareAction { public RefreshAction() { super("Reload List of Plugins", "Reload list of plugins", AllIcons.Actions.Refresh); } @Override public void actionPerformed(AnActionEvent e) { loadAvailablePlugins(); myFilter.setFilter(""); } @Override public void update(AnActionEvent e) { e.getPresentation().setEnabled(!myBusy); } } protected DefaultActionGroup createSortersGroup() { final DefaultActionGroup group = new DefaultActionGroup("Sort by", true); group.addAction(new SortByStatusAction(pluginTable, pluginsModel)); return group; } }