set repository name for plugins from external repositories (IDEA-80276)
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / updateSettings / impl / UpdateChecker.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.updateSettings.impl;
17
18 import com.intellij.ide.IdeBundle;
19 import com.intellij.ide.plugins.*;
20 import com.intellij.ide.reporter.ConnectionException;
21 import com.intellij.ide.util.PropertiesComponent;
22 import com.intellij.notification.Notification;
23 import com.intellij.notification.NotificationType;
24 import com.intellij.notification.Notifications;
25 import com.intellij.openapi.application.ApplicationInfo;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.application.PathManager;
28 import com.intellij.openapi.application.ex.ApplicationInfoEx;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.extensions.PluginId;
31 import com.intellij.openapi.progress.ProcessCanceledException;
32 import com.intellij.openapi.progress.ProgressIndicator;
33 import com.intellij.openapi.progress.ProgressManager;
34 import com.intellij.openapi.ui.Messages;
35 import com.intellij.openapi.util.BuildNumber;
36 import com.intellij.openapi.util.JDOMUtil;
37 import com.intellij.openapi.util.SystemInfo;
38 import com.intellij.openapi.util.io.FileUtil;
39 import com.intellij.openapi.util.text.StringUtil;
40 import com.intellij.openapi.vfs.VirtualFile;
41 import com.intellij.util.PlatformUtils;
42 import com.intellij.util.containers.ContainerUtil;
43 import com.intellij.util.io.UrlConnectionUtil;
44 import com.intellij.util.net.HttpConfigurable;
45 import com.intellij.util.text.DateFormatUtil;
46 import com.intellij.util.ui.UIUtil;
47 import org.jdom.Document;
48 import org.jdom.Element;
49 import org.jdom.JDOMException;
50 import org.jetbrains.annotations.NonNls;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import javax.swing.*;
55 import java.io.*;
56 import java.net.HttpURLConnection;
57 import java.net.URL;
58 import java.net.URLConnection;
59 import java.util.*;
60 import java.util.concurrent.Future;
61 import java.util.concurrent.TimeUnit;
62 import java.util.concurrent.TimeoutException;
63
64 /**
65  * XML sample:
66  * <pre>{@code
67  * <idea>
68  *   <build>456</build>
69  *   <version>4.5.2</version>
70  *   <title>New Intellij IDEA Version</title>
71  *   <message>
72  *     New version of IntelliJ IDEA is available.
73  *     Please visit http://www.intellij.com/ for more info.
74  *   </message>
75  * </idea>
76  * }</pre>
77  *
78  * @author mike
79  * Date: Oct 31, 2002
80  */
81 public final class UpdateChecker {
82   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.updateSettings.impl.UpdateChecker");
83
84   public static String ADDITIONAL_REQUEST_OPTIONS = "";
85   @NonNls private static final String INSTALLATION_UID = "installation.uid";
86
87   private UpdateChecker() {
88   }
89
90   public static void showConnectionErrorDialog() {
91     Messages.showErrorDialog(IdeBundle.message("error.checkforupdates.connection.failed"),
92                              IdeBundle.message("title.connection.error"));
93   }
94
95   public static enum DownloadPatchResult {
96     SUCCESS, FAILED, CANCELED
97   }
98
99   private static boolean myVeryFirstOpening = true;
100
101
102   @NonNls
103   private static final String DISABLED_UPDATE = "disabled_update.txt";
104   private static TreeSet<String> ourDisabledToUpdatePlugins;
105
106   private static class StringHolder {
107     private static final String UPDATE_URL = ApplicationInfoEx.getInstanceEx().getUpdateUrls().getCheckingUrl();
108     private static final String PATCHES_URL = ApplicationInfoEx.getInstanceEx().getUpdateUrls().getPatchesUrl();
109     private StringHolder() { }
110   }
111
112   private static String getUpdateUrl() {
113     String url = System.getProperty("idea.updates.url");
114     if (url != null) {
115       return url;
116     }
117     return StringHolder.UPDATE_URL;
118   }
119
120   private static String getPatchesUrl() {
121     String url = System.getProperty("idea.patches.url");
122     if (url != null) {
123       return url;
124     }
125     return StringHolder.PATCHES_URL;
126   }
127
128   public static boolean isMyVeryFirstOpening() {
129     return myVeryFirstOpening;
130   }
131
132   public static void setMyVeryFirstOpening(final boolean myVeryFirstProjectOpening) {
133     myVeryFirstOpening = myVeryFirstProjectOpening;
134   }
135
136   public static boolean checkNeeded() {
137     final UpdateSettings settings = UpdateSettings.getInstance();
138     if (settings == null || getUpdateUrl() == null) return false;
139
140     final long timeDelta = System.currentTimeMillis() - settings.LAST_TIME_CHECKED;
141     if (Math.abs(timeDelta) < DateFormatUtil.DAY) return false;
142
143     return settings.CHECK_NEEDED;
144   }
145
146   public static List<PluginDownloader> updatePlugins(final boolean showErrorDialog,
147                                                      final @Nullable PluginHostsConfigurable hostsConfigurable) {
148     final List<PluginDownloader> downloaded = new ArrayList<PluginDownloader>();
149     final Set<String> failed = new HashSet<String>();
150     for (String host : getPluginHosts(hostsConfigurable)) {
151       try {
152         checkPluginsHost(host, downloaded);
153       }
154       catch (Exception e) {
155         LOG.info(e);
156         failed.add(host);
157       }
158     }
159
160     final Map<String, IdeaPluginDescriptor> toUpdate = new HashMap<String, IdeaPluginDescriptor>();
161     final IdeaPluginDescriptor[] installedPlugins = PluginManager.getPlugins();
162     for (IdeaPluginDescriptor installedPlugin : installedPlugins) {
163       if (!installedPlugin.isBundled()) {
164         toUpdate.put(installedPlugin.getPluginId().getIdString(), installedPlugin);
165       }
166     }
167     final PluginManagerUISettings updateSettings = PluginManagerUISettings.getInstance();
168     updateSettings.myOutdatedPlugins.clear();
169     if (!toUpdate.isEmpty()) {
170       try {
171         final ArrayList<IdeaPluginDescriptor> process = RepositoryHelper.process(null);
172         for (IdeaPluginDescriptor loadedPlugin : process) {
173           final String idString = loadedPlugin.getPluginId().getIdString();
174           final IdeaPluginDescriptor installedPlugin = toUpdate.get(idString);
175           if (installedPlugin != null) {
176             if (StringUtil.compareVersionNumbers(loadedPlugin.getVersion(), installedPlugin.getVersion()) > 0) {
177               updateSettings.myOutdatedPlugins.add(idString);
178               if (((IdeaPluginDescriptorImpl)installedPlugin).isEnabled()) {
179                 final PluginDownloader downloader = PluginDownloader.createDownloader(loadedPlugin);
180                 if (downloader.prepareToInstall()) {
181                   downloaded.add(downloader);
182                 }
183               }
184             }
185           }
186         }
187       }
188       catch (Exception e) {
189         showErrorMessage(showErrorDialog, e.getMessage());
190       }
191     }
192
193     if (!failed.isEmpty()) {
194       showErrorMessage(showErrorDialog, IdeBundle.message("connection.failed.message", StringUtil.join(failed, ",")));
195     }
196     return downloaded.isEmpty() ? null : downloaded;
197   }
198
199   private static void showErrorMessage(boolean showErrorDialog, final String failedMessage) {
200     if (showErrorDialog) {
201       UIUtil.invokeLaterIfNeeded(new Runnable() {
202         @Override
203         public void run() {
204           Messages.showErrorDialog(failedMessage, IdeBundle.message("title.connection.error"));
205         }
206       });
207     }
208     else {
209       LOG.info(failedMessage);
210     }
211   }
212
213   private static List<String> getPluginHosts(@Nullable PluginHostsConfigurable hostsConfigurable) {
214     final ArrayList<String> hosts = new ArrayList<String>();
215     if (hostsConfigurable != null) {
216       hosts.addAll(hostsConfigurable.getPluginsHosts());
217     }
218     else {
219       hosts.addAll(UpdateSettings.getInstance().myPluginHosts);
220     }
221     final String pluginHosts = System.getProperty("idea.plugin.hosts");
222     if (pluginHosts != null) {
223       ContainerUtil.addAll(hosts, pluginHosts.split(";"));
224     }
225     return hosts;
226   }
227
228   public static boolean checkPluginsHost(final String host, final List<PluginDownloader> downloaded) throws Exception {
229     return checkPluginsHost(host, downloaded, true);
230   }
231
232   public static boolean checkPluginsHost(final String host,
233                                          final List<PluginDownloader> downloaded,
234                                          final boolean collectToUpdate) throws Exception {
235     InputStream inputStream = loadVersionInfo(host);
236     if (inputStream == null) return false;
237     final Document document;
238     try {
239       document = JDOMUtil.loadDocument(inputStream);
240     }
241     catch (JDOMException e) {
242       return false;
243     }
244
245     inputStream = loadVersionInfo(host);
246     if (inputStream == null) return false;
247     final ArrayList<IdeaPluginDescriptor> descriptors = RepositoryHelper.loadPluginsFromDescription(inputStream);
248     for (IdeaPluginDescriptor descriptor : descriptors) {
249       ((PluginNode)descriptor).setRepositoryName(host);
250       downloaded.add(PluginDownloader.createDownloader(descriptor));
251     }
252
253     boolean success = true;
254     for (Object plugin : document.getRootElement().getChildren("plugin")) {
255       final Element pluginElement = (Element)plugin;
256       final String pluginId = pluginElement.getAttributeValue("id");
257       final String pluginUrl = pluginElement.getAttributeValue("url");
258       final String pluginVersion = pluginElement.getAttributeValue("version");
259       final Element descriptionElement = pluginElement.getChild("description");
260       final String description;
261       if (descriptionElement != null) {
262         description = descriptionElement.getText();
263       } else {
264         description = null;
265       }
266       
267       final List<PluginId> dependsPlugins = new ArrayList<PluginId>();
268       final List depends = pluginElement.getChildren("depends");
269       for (Object depend : depends) {
270         dependsPlugins.add(PluginId.getId(((Element)depend).getText()));
271       }
272
273       if (pluginId == null) {
274         LOG.info("plugin id should not be null");
275         success = false;
276         continue;
277       }
278
279       if (pluginUrl == null) {
280         LOG.info("plugin url should not be null");
281         success = false;
282         continue;
283       }
284
285       final VirtualFile pluginFile = PluginDownloader.findPluginFile(pluginUrl, host);
286       if (pluginFile == null) continue;
287
288       if (collectToUpdate) {
289         final String finalPluginUrl = pluginFile.getUrl();
290         final Runnable updatePluginRunnable = new Runnable() {
291           public void run() {
292             try {
293               final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
294               if (progressIndicator != null) {
295                 progressIndicator.setText(finalPluginUrl);
296               }
297               final PluginDownloader uploader = new PluginDownloader(pluginId, finalPluginUrl, pluginVersion);
298               if (uploader.prepareToInstall()) {
299                 downloaded.add(uploader);
300               }
301             }
302             catch (IOException e) {
303               LOG.info(e);
304             }
305           }
306         };
307         if (ApplicationManager.getApplication().isDispatchThread()) {
308           ProgressManager.getInstance().runProcessWithProgressSynchronously(updatePluginRunnable, IdeBundle.message("update.uploading.plugin.progress.title"), true, null);
309         }
310         else {
311           updatePluginRunnable.run();
312         }
313       } else {
314         final PluginDownloader downloader = new PluginDownloader(pluginId, pluginUrl, pluginVersion);
315         downloader.setDescription(description);
316         downloader.setDepends(dependsPlugins);
317         downloaded.add(downloader);
318       }
319     }
320     return success;
321   }
322
323   @NotNull
324   public static CheckForUpdateResult doCheckForUpdates(final UpdateSettings settings) {
325     ApplicationInfo appInfo = ApplicationInfo.getInstance();
326     BuildNumber currentBuild = appInfo.getBuild();
327     int majorVersion = Integer.parseInt(appInfo.getMajorVersion());
328     final UpdatesXmlLoader loader = new UpdatesXmlLoader(getUpdateUrl(), getInstallationUID(), null);
329     final UpdatesInfo info;
330     try {
331       info = loader.loadUpdatesInfo();
332       if (info == null) {
333         return new CheckForUpdateResult(UpdateStrategy.State.NOTHING_LOADED);
334       }
335     }
336     catch (ConnectionException e) {
337       return new CheckForUpdateResult(UpdateStrategy.State.CONNECTION_ERROR, e);
338     }
339
340     UpdateStrategy strategy = new UpdateStrategy(majorVersion, currentBuild, info, settings);
341     return strategy.checkForUpdates();
342   }
343
344
345   @NotNull
346   public static CheckForUpdateResult checkForUpdates() {
347     return checkForUpdates(false);
348   }
349
350   @NotNull
351   public static CheckForUpdateResult checkForUpdates(final boolean disregardIgnoredBuilds) {
352     if (LOG.isDebugEnabled()) {
353       LOG.debug("enter: auto checkForUpdates()");
354     }
355
356     UserUpdateSettings settings = UpdateSettings.getInstance();
357     if (disregardIgnoredBuilds) {
358       settings = new UserUpdateSettings() {
359         @NotNull
360         @Override
361         public List<String> getKnownChannelsIds() {
362           return UpdateSettings.getInstance().getKnownChannelsIds();
363         }
364
365         @Override
366         public List<String> getIgnoredBuildNumbers() {
367           return Collections.emptyList();
368         }
369
370         @Override
371         public void setKnownChannelIds(List<String> ids) {
372           UpdateSettings.getInstance().setKnownChannelIds(ids);
373         }
374
375         @NotNull
376         @Override
377         public ChannelStatus getSelectedChannelStatus() {
378           return UpdateSettings.getInstance().getSelectedChannelStatus();
379         }
380       };
381     }
382
383     final CheckForUpdateResult result = doCheckForUpdates(UpdateSettings.getInstance());
384
385     if (result.getState() == UpdateStrategy.State.LOADED) {
386       UpdateSettings.getInstance().LAST_TIME_CHECKED = System.currentTimeMillis();
387       settings.setKnownChannelIds(result.getAllChannelsIds());
388     }
389
390     return result;
391   }
392
393   public static void showUpdateResult(CheckForUpdateResult checkForUpdateResult,
394                                       List<PluginDownloader> updatedPlugins,
395                                       boolean showConfirmation,
396                                       boolean enableLink,
397                                       final boolean alwaysShowResults) {
398     UpdateChannel channelToPropose = checkForUpdateResult.getChannelToPropose();
399     if (channelToPropose != null && channelToPropose.getLatestBuild() != null) {
400       NewChannelDialog dialog = new NewChannelDialog(channelToPropose);
401       dialog.setModal(alwaysShowResults);
402       dialog.show();
403     }
404     else if (checkForUpdateResult.hasNewBuildInSelectedChannel()) {
405       UpdateInfoDialog dialog = new UpdateInfoDialog(true, checkForUpdateResult.getUpdatedChannel(), updatedPlugins, enableLink);
406       dialog.setModal(alwaysShowResults);
407       dialog.show();
408     }
409     else if (updatedPlugins != null || alwaysShowResults) {
410       NoUpdatesDialog dialog = new NoUpdatesDialog(true, updatedPlugins, enableLink);
411       dialog.setShowConfirmation(showConfirmation);
412       dialog.show();
413     }
414 }
415
416   private static InputStream loadVersionInfo(final String url) throws Exception {
417     if (LOG.isDebugEnabled()) {
418       LOG.debug("enter: loadVersionInfo(UPDATE_URL='" + url + "' )");
419     }
420     final InputStream[] inputStreams = new InputStream[]{null};
421     final Exception[] exception = new Exception[]{null};
422     Future<?> downloadThreadFuture = ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
423       public void run() {
424         try {
425           HttpConfigurable.getInstance().prepareURL(url);
426
427           String uid = getInstallationUID();
428
429           final URL requestUrl =
430             new URL(url + "?build=" + ApplicationInfo.getInstance().getBuild().asString() + "&uid=" + uid + ADDITIONAL_REQUEST_OPTIONS);
431           inputStreams[0] = requestUrl.openStream();
432         }
433         catch (IOException e) {
434           exception[0] = e;
435         }
436       }
437     });
438
439     try {
440       downloadThreadFuture.get(5, TimeUnit.SECONDS);
441     }
442     catch (TimeoutException e) {
443       // ignore
444     }
445
446     if (!downloadThreadFuture.isDone()) {
447       downloadThreadFuture.cancel(true);
448       throw new ConnectionException(IdeBundle.message("updates.timeout.error"));
449     }
450
451     if (exception[0] != null) throw exception[0];
452     return inputStreams[0];
453   }
454
455   public static String getInstallationUID() {
456     final PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
457     String uid = "";
458     if (!propertiesComponent.isValueSet(INSTALLATION_UID)) {
459       try {
460         uid = UUID.randomUUID().toString();
461       }
462       catch (Exception ignored) {
463       }
464       catch (InternalError ignored) {
465       }
466       propertiesComponent.setValue(INSTALLATION_UID, uid);
467     }
468     else {
469       uid = propertiesComponent.getValue(INSTALLATION_UID);
470     }
471     return uid;
472   }
473
474   public static boolean install(List<PluginDownloader> downloaders) {
475     boolean installed = false;
476     for (PluginDownloader downloader : downloaders) {
477       if (getDisabledToUpdatePlugins().contains(downloader.getPluginId())) continue;
478       try {
479         downloader.install();
480         installed = true;
481       }
482       catch (IOException e) {
483         LOG.info(e);
484       }
485     }
486     return installed;
487   }
488
489
490   public static DownloadPatchResult downloadAndInstallPatch(final BuildInfo newVersion) {
491     final DownloadPatchResult[] result = new DownloadPatchResult[]{DownloadPatchResult.CANCELED};
492
493     if (!ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
494       public void run() {
495         try {
496           doDownloadAndInstallPatch(newVersion, ProgressManager.getInstance().getProgressIndicator());
497           result[0] = DownloadPatchResult.SUCCESS;
498         }
499         catch (final IOException e) {
500           LOG.info(e);
501           result[0] = DownloadPatchResult.FAILED;
502
503           SwingUtilities.invokeLater(new Runnable() {
504             public void run() {
505               Notifications.Bus.notify(new Notification("Updater",
506                                                         "Failed to download patch file",
507                                                         e.getMessage(),
508                                                         NotificationType.ERROR));
509             }
510           });
511         }
512       }
513     }, IdeBundle.message("update.downloading.patch.progress.title"), true, null)) {
514       return DownloadPatchResult.CANCELED;
515     }
516
517     return result[0];
518   }
519
520   private static void doDownloadAndInstallPatch(BuildInfo newVersion, ProgressIndicator i) throws IOException {
521     PatchInfo patch = newVersion.findPatchForCurrentBuild();
522     if (patch == null) throw new IOException("No patch is available for current version");
523
524     String productCode = ApplicationInfo.getInstance().getBuild().getProductCode();
525
526     String osSuffix = "";
527     if (SystemInfo.isWindows) {
528       osSuffix = "-win";
529     }
530     else if (SystemInfo.isMac) {
531       osSuffix = "-mac";
532     }
533     else if (SystemInfo.isUnix) osSuffix = "-unix";
534
535     String fromBuildNumber = patch.getFromBuild().asStringWithoutProductCode();
536     String toBuildNumber = newVersion.getNumber().asStringWithoutProductCode();
537     String fileName = productCode + "-" + fromBuildNumber + "-" + toBuildNumber + "-patch" + osSuffix + ".jar";
538     URLConnection connection = null;
539     InputStream in = null;
540     OutputStream out = null;
541
542     String platform = PlatformUtils.getPlatformPrefix();
543     String patchFileName = ("jetbrains.patch.jar." + platform).toLowerCase();
544     File patchFile = new File(FileUtil.getTempDirectory(), patchFileName);
545
546     try {
547       connection = new URL(new URL(getPatchesUrl()), fileName).openConnection();
548       in = UrlConnectionUtil.getConnectionInputStreamWithException(connection, i);
549       out = new BufferedOutputStream(new FileOutputStream(patchFile));
550
551       i.setIndeterminate(false);
552
553       byte[] buffer = new byte[10 * 1024];
554       int total = connection.getContentLength();
555       int count;
556       int read = 0;
557
558       while ((count = in.read(buffer)) > 0) {
559         i.checkCanceled();
560         out.write(buffer, 0, count);
561         read += count;
562         i.setFraction(((double)read) / total);
563         i.setText2((read / 1024) + "/" + (total / 1024) + " KB");
564       }
565     }
566     catch (IOException e) {
567       patchFile.delete();
568       throw e;
569     }
570     catch (ProcessCanceledException e) {
571       patchFile.delete();
572       throw e;
573     }
574     catch (Throwable e) {
575       patchFile.delete();
576       throw new RuntimeException(e);
577     }
578     finally {
579       if (out != null) out.close();
580       if (in != null) in.close();
581       if (connection instanceof HttpURLConnection) ((HttpURLConnection)connection).disconnect();
582     }
583   }
584
585   public static Set<String> getDisabledToUpdatePlugins() {
586     if (ourDisabledToUpdatePlugins == null) {
587       ourDisabledToUpdatePlugins = new TreeSet<String>();
588       if (!ApplicationManager.getApplication().isUnitTestMode()) {
589         try {
590           final File file = new File(PathManager.getConfigPath(), DISABLED_UPDATE);
591           if (file.isFile()) {
592             final String[] ids = FileUtil.loadFile(file).split("[\\s]");
593             for (String id : ids) {
594               if (id != null && id.trim().length() > 0) {
595                 ourDisabledToUpdatePlugins.add(id.trim());
596               }
597             }
598           }
599         }
600         catch (IOException e) {
601           LOG.error(e);
602         }
603       }
604     }
605     return ourDisabledToUpdatePlugins;
606   }
607
608   public static void saveDisabledToUpdatePlugins() {
609     try {
610       File plugins = new File(PathManager.getConfigPath(), DISABLED_UPDATE);
611       FileUtil.ensureCanCreateFile(plugins);
612
613       PrintWriter printWriter = null;
614       try {
615         printWriter = new PrintWriter(new BufferedWriter(new FileWriter(plugins)));
616         for (String id : getDisabledToUpdatePlugins()) {
617           printWriter.println(id);
618         }
619         printWriter.flush();
620       }
621       finally {
622         if (printWriter != null) {
623           printWriter.close();
624         }
625       }
626     }
627     catch (IOException e) {
628       LOG.error(e);
629     }
630   }
631 }