001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.io.File; 008import java.io.IOException; 009import java.io.InputStream; 010import java.net.MalformedURLException; 011import java.net.URL; 012import java.nio.file.Files; 013import java.nio.file.StandardCopyOption; 014import java.util.Collection; 015import java.util.LinkedList; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.data.Version; 019import org.openstreetmap.josm.gui.ExtendedDialog; 020import org.openstreetmap.josm.gui.PleaseWaitRunnable; 021import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 022import org.openstreetmap.josm.gui.progress.ProgressMonitor; 023import org.openstreetmap.josm.tools.CheckParameterUtil; 024import org.openstreetmap.josm.tools.HttpClient; 025import org.xml.sax.SAXException; 026 027/** 028 * Asynchronous task for downloading a collection of plugins. 029 * 030 * When the task is finished {@link #getDownloadedPlugins()} replies the list of downloaded plugins 031 * and {@link #getFailedPlugins()} replies the list of failed plugins. 032 * 033 */ 034public class PluginDownloadTask extends PleaseWaitRunnable { 035 036 /** 037 * The accepted MIME types sent in the HTTP Accept header. 038 * @since 6867 039 */ 040 public static final String PLUGIN_MIME_TYPES = "application/java-archive, application/zip; q=0.9, application/octet-stream; q=0.5"; 041 042 private final Collection<PluginInformation> toUpdate = new LinkedList<>(); 043 private final Collection<PluginInformation> failed = new LinkedList<>(); 044 private final Collection<PluginInformation> downloaded = new LinkedList<>(); 045 private boolean canceled; 046 private HttpClient downloadConnection; 047 048 /** 049 * Creates the download task 050 * 051 * @param parent the parent component relative to which the {@link org.openstreetmap.josm.gui.PleaseWaitDialog} is displayed 052 * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null. 053 * @param title the title to display in the {@link org.openstreetmap.josm.gui.PleaseWaitDialog} 054 * @throws IllegalArgumentException if toUpdate is null 055 */ 056 public PluginDownloadTask(Component parent, Collection<PluginInformation> toUpdate, String title) { 057 super(parent, title == null ? "" : title, false /* don't ignore exceptions */); 058 CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate"); 059 this.toUpdate.addAll(toUpdate); 060 } 061 062 /** 063 * Creates the task 064 * 065 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 066 * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null. 067 * @param title the title to display in the {@link org.openstreetmap.josm.gui.PleaseWaitDialog} 068 * @throws IllegalArgumentException if toUpdate is null 069 */ 070 public PluginDownloadTask(ProgressMonitor monitor, Collection<PluginInformation> toUpdate, String title) { 071 super(title, monitor == null ? NullProgressMonitor.INSTANCE : monitor, false /* don't ignore exceptions */); 072 CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate"); 073 this.toUpdate.addAll(toUpdate); 074 } 075 076 /** 077 * Sets the collection of plugins to update. 078 * 079 * @param toUpdate the collection of plugins to update. Must not be null. 080 * @throws IllegalArgumentException if toUpdate is null 081 */ 082 public void setPluginsToDownload(Collection<PluginInformation> toUpdate) { 083 CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate"); 084 this.toUpdate.clear(); 085 this.toUpdate.addAll(toUpdate); 086 } 087 088 @Override 089 protected void cancel() { 090 this.canceled = true; 091 synchronized (this) { 092 if (downloadConnection != null) { 093 downloadConnection.disconnect(); 094 } 095 } 096 } 097 098 @Override 099 protected void finish() {} 100 101 protected void download(PluginInformation pi, File file) throws PluginDownloadException { 102 if (pi.mainversion > Version.getInstance().getVersion()) { 103 ExtendedDialog dialog = new ExtendedDialog( 104 progressMonitor.getWindowParent(), 105 tr("Skip download"), 106 new String[] { 107 tr("Download Plugin"), 108 tr("Skip Download") } 109 ); 110 dialog.setContent(tr("JOSM version {0} required for plugin {1}.", pi.mainversion, pi.name)); 111 dialog.setButtonIcons(new String[] {"download", "cancel"}); 112 dialog.showDialog(); 113 int answer = dialog.getValue(); 114 if (answer != 1) 115 throw new PluginDownloadException(tr("Download skipped")); 116 } 117 try { 118 if (pi.downloadlink == null) { 119 String msg = tr("Cannot download plugin ''{0}''. Its download link is not known. Skipping download.", pi.name); 120 Main.warn(msg); 121 throw new PluginDownloadException(msg); 122 } 123 URL url = new URL(pi.downloadlink); 124 synchronized (this) { 125 downloadConnection = HttpClient.create(url) 126 .setAccept(PLUGIN_MIME_TYPES); 127 downloadConnection.connect(); 128 } 129 try (InputStream in = downloadConnection.getResponse().getContent()) { 130 Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING); 131 } 132 } catch (MalformedURLException e) { 133 String msg = tr("Cannot download plugin ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.", 134 pi.name, pi.downloadlink); 135 Main.warn(msg); 136 throw new PluginDownloadException(msg, e); 137 } catch (IOException e) { 138 if (canceled) 139 return; 140 throw new PluginDownloadException(e); 141 } finally { 142 synchronized (this) { 143 downloadConnection = null; 144 } 145 } 146 } 147 148 @Override 149 protected void realRun() throws SAXException, IOException { 150 File pluginDir = Main.pref.getPluginsDirectory(); 151 if (!pluginDir.exists() && !pluginDir.mkdirs()) { 152 /*lastException =*/ new PluginDownloadException(tr("Failed to create plugin directory ''{0}''", pluginDir.toString())); 153 failed.addAll(toUpdate); 154 return; 155 } 156 getProgressMonitor().setTicksCount(toUpdate.size()); 157 for (PluginInformation d : toUpdate) { 158 if (canceled) return; 159 String message = tr("Downloading Plugin {0}...", d.name); 160 Main.info(message); 161 progressMonitor.subTask(message); 162 progressMonitor.worked(1); 163 File pluginFile = new File(pluginDir, d.name + ".jar.new"); 164 try { 165 download(d, pluginFile); 166 } catch (PluginDownloadException e) { 167 Main.error(e); 168 failed.add(d); 169 continue; 170 } 171 downloaded.add(d); 172 } 173 PluginHandler.installDownloadedPlugins(false); 174 } 175 176 /** 177 * Replies true if the task was canceled by the user 178 * 179 * @return <code>true</code> if the task was stopped by the user 180 */ 181 public boolean isCanceled() { 182 return canceled; 183 } 184 185 /** 186 * Replies the list of plugins whose download has failed. 187 * 188 * @return the list of plugins whose download has failed 189 */ 190 public Collection<PluginInformation> getFailedPlugins() { 191 return failed; 192 } 193 194 /** 195 * Replies the list of successfully downloaded plugins. 196 * 197 * @return the list of successfully downloaded plugins 198 */ 199 public Collection<PluginInformation> getDownloadedPlugins() { 200 return downloaded; 201 } 202}