/*
* 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 final NotificationGroup PLUGIN_LIFECYCLE_NOTIFICATION_GROUP =
new NotificationGroup("Plugins Lifecycle Group", NotificationDisplayType.STICKY_BALLOON, true);
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 += "";
PLUGIN_LIFECYCLE_NOTIFICATION_GROUP
.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;
}
}