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.Dimension; 007import java.awt.GridBagLayout; 008import java.io.ByteArrayInputStream; 009import java.io.File; 010import java.io.FileOutputStream; 011import java.io.FilenameFilter; 012import java.io.IOException; 013import java.io.InputStream; 014import java.io.OutputStreamWriter; 015import java.io.PrintWriter; 016import java.net.MalformedURLException; 017import java.net.URL; 018import java.nio.charset.StandardCharsets; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashSet; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Set; 027 028import javax.swing.JLabel; 029import javax.swing.JOptionPane; 030import javax.swing.JPanel; 031import javax.swing.JScrollPane; 032 033import org.openstreetmap.josm.Main; 034import org.openstreetmap.josm.gui.PleaseWaitRunnable; 035import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 036import org.openstreetmap.josm.gui.progress.ProgressMonitor; 037import org.openstreetmap.josm.gui.util.GuiHelper; 038import org.openstreetmap.josm.gui.widgets.JosmTextArea; 039import org.openstreetmap.josm.io.OsmTransferException; 040import org.openstreetmap.josm.tools.GBC; 041import org.openstreetmap.josm.tools.HttpClient; 042import org.openstreetmap.josm.tools.Utils; 043import org.xml.sax.SAXException; 044 045/** 046 * An asynchronous task for downloading plugin lists from the configured plugin download sites. 047 * @since 2817 048 */ 049public class ReadRemotePluginInformationTask extends PleaseWaitRunnable { 050 051 private Collection<String> sites; 052 private boolean canceled; 053 private HttpClient connection; 054 private List<PluginInformation> availablePlugins; 055 private boolean displayErrMsg; 056 057 protected final void init(Collection<String> sites, boolean displayErrMsg) { 058 this.sites = sites; 059 if (sites == null) { 060 this.sites = Collections.emptySet(); 061 } 062 this.availablePlugins = new LinkedList<>(); 063 this.displayErrMsg = displayErrMsg; 064 } 065 066 /** 067 * Constructs a new {@code ReadRemotePluginInformationTask}. 068 * 069 * @param sites the collection of download sites. Defaults to the empty collection if null. 070 */ 071 public ReadRemotePluginInformationTask(Collection<String> sites) { 072 super(tr("Download plugin list..."), false /* don't ignore exceptions */); 073 init(sites, true); 074 } 075 076 /** 077 * Constructs a new {@code ReadRemotePluginInformationTask}. 078 * 079 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 080 * @param sites the collection of download sites. Defaults to the empty collection if null. 081 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 082 */ 083 public ReadRemotePluginInformationTask(ProgressMonitor monitor, Collection<String> sites, boolean displayErrMsg) { 084 super(tr("Download plugin list..."), monitor == null ? NullProgressMonitor.INSTANCE : monitor, false /* don't ignore exceptions */); 085 init(sites, displayErrMsg); 086 } 087 088 @Override 089 protected void cancel() { 090 canceled = true; 091 synchronized (this) { 092 if (connection != null) { 093 connection.disconnect(); 094 } 095 } 096 } 097 098 @Override 099 protected void finish() {} 100 101 /** 102 * Creates the file name for the cached plugin list and the icon cache file. 103 * 104 * @param pluginDir directory of plugin for data storage 105 * @param site the name of the site 106 * @return the file name for the cache file 107 */ 108 protected File createSiteCacheFile(File pluginDir, String site) { 109 String name; 110 try { 111 site = site.replaceAll("%<(.*)>", ""); 112 URL url = new URL(site); 113 StringBuilder sb = new StringBuilder(); 114 sb.append("site-") 115 .append(url.getHost()).append('-'); 116 if (url.getPort() != -1) { 117 sb.append(url.getPort()).append('-'); 118 } 119 String path = url.getPath(); 120 for (int i = 0; i < path.length(); i++) { 121 char c = path.charAt(i); 122 if (Character.isLetterOrDigit(c)) { 123 sb.append(c); 124 } else { 125 sb.append('_'); 126 } 127 } 128 sb.append(".txt"); 129 name = sb.toString(); 130 } catch (MalformedURLException e) { 131 name = "site-unknown.txt"; 132 } 133 return new File(pluginDir, name); 134 } 135 136 /** 137 * Downloads the list from a remote location 138 * 139 * @param site the site URL 140 * @param monitor a progress monitor 141 * @return the downloaded list 142 */ 143 protected String downloadPluginList(String site, final ProgressMonitor monitor) { 144 /* replace %<x> with empty string or x=plugins (separated with comma) */ 145 String pl = Utils.join(",", Main.pref.getCollection("plugins")); 146 String printsite = site.replaceAll("%<(.*)>", ""); 147 if (pl != null && !pl.isEmpty()) { 148 site = site.replaceAll("%<(.*)>", "$1"+pl); 149 } else { 150 site = printsite; 151 } 152 153 String content = null; 154 try { 155 monitor.beginTask(""); 156 monitor.indeterminateSubTask(tr("Downloading plugin list from ''{0}''", printsite)); 157 158 URL url = new URL(site); 159 connection = HttpClient.create(url).useCache(false); 160 final HttpClient.Response response = connection.connect(); 161 content = response.fetchContent(); 162 if (response.getResponseCode() != 200) { 163 throw new IOException(tr("Unsuccessful HTTP request")); 164 } 165 return content; 166 } catch (MalformedURLException e) { 167 if (canceled) return null; 168 Main.error(e); 169 return null; 170 } catch (IOException e) { 171 if (canceled) return null; 172 handleIOException(monitor, e, content); 173 return null; 174 } finally { 175 synchronized (this) { 176 if (connection != null) { 177 connection.disconnect(); 178 } 179 connection = null; 180 } 181 monitor.finishTask(); 182 } 183 } 184 185 private void handleIOException(final ProgressMonitor monitor, IOException e, String details) { 186 final String msg = e.getMessage(); 187 if (details == null || details.isEmpty()) { 188 Main.error(e.getClass().getSimpleName()+": " + msg); 189 } else { 190 Main.error(msg + " - Details:\n" + details); 191 } 192 193 if (displayErrMsg) { 194 displayErrorMessage(monitor, msg, details, tr("Plugin list download error"), tr("JOSM failed to download plugin list:")); 195 } 196 } 197 198 private void displayErrorMessage(final ProgressMonitor monitor, final String msg, final String details, final String title, 199 final String firstMessage) { 200 GuiHelper.runInEDTAndWait(new Runnable() { 201 @Override public void run() { 202 JPanel panel = new JPanel(new GridBagLayout()); 203 panel.add(new JLabel(firstMessage), GBC.eol().insets(0, 0, 0, 10)); 204 StringBuilder b = new StringBuilder(); 205 for (String part : msg.split("(?<=\\G.{200})")) { 206 b.append(part).append('\n'); 207 } 208 panel.add(new JLabel("<html><body width=\"500\"><b>"+b.toString().trim()+"</b></body></html>"), GBC.eol().insets(0, 0, 0, 10)); 209 if (details != null && !details.isEmpty()) { 210 panel.add(new JLabel(tr("Details:")), GBC.eol().insets(0, 0, 0, 10)); 211 JosmTextArea area = new JosmTextArea(details); 212 area.setEditable(false); 213 area.setLineWrap(true); 214 area.setWrapStyleWord(true); 215 JScrollPane scrollPane = new JScrollPane(area); 216 scrollPane.setPreferredSize(new Dimension(500, 300)); 217 panel.add(scrollPane, GBC.eol().fill()); 218 } 219 JOptionPane.showMessageDialog(monitor.getWindowParent(), panel, title, JOptionPane.ERROR_MESSAGE); 220 } 221 }); 222 } 223 224 /** 225 * Writes the list of plugins to a cache file 226 * 227 * @param site the site from where the list was downloaded 228 * @param list the downloaded list 229 */ 230 protected void cachePluginList(String site, String list) { 231 File pluginDir = Main.pref.getPluginsDirectory(); 232 if (!pluginDir.exists() && !pluginDir.mkdirs()) { 233 Main.warn(tr("Failed to create plugin directory ''{0}''. Cannot cache plugin list from plugin site ''{1}''.", 234 pluginDir.toString(), site)); 235 } 236 File cacheFile = createSiteCacheFile(pluginDir, site); 237 getProgressMonitor().subTask(tr("Writing plugin list to local cache ''{0}''", cacheFile.toString())); 238 try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(cacheFile), StandardCharsets.UTF_8))) { 239 writer.write(list); 240 writer.flush(); 241 } catch (IOException e) { 242 // just failed to write the cache file. No big deal, but log the exception anyway 243 Main.error(e); 244 } 245 } 246 247 /** 248 * Filter information about deprecated plugins from the list of downloaded 249 * plugins 250 * 251 * @param plugins the plugin informations 252 * @return the plugin informations, without deprecated plugins 253 */ 254 protected List<PluginInformation> filterDeprecatedPlugins(List<PluginInformation> plugins) { 255 List<PluginInformation> ret = new ArrayList<>(plugins.size()); 256 Set<String> deprecatedPluginNames = new HashSet<>(); 257 for (PluginHandler.DeprecatedPlugin p : PluginHandler.DEPRECATED_PLUGINS) { 258 deprecatedPluginNames.add(p.name); 259 } 260 for (PluginInformation plugin: plugins) { 261 if (deprecatedPluginNames.contains(plugin.name)) { 262 continue; 263 } 264 ret.add(plugin); 265 } 266 return ret; 267 } 268 269 /** 270 * Parses the plugin list 271 * 272 * @param site the site from where the list was downloaded 273 * @param doc the document with the plugin list 274 */ 275 protected void parsePluginListDocument(String site, String doc) { 276 try { 277 getProgressMonitor().subTask(tr("Parsing plugin list from site ''{0}''", site)); 278 InputStream in = new ByteArrayInputStream(doc.getBytes(StandardCharsets.UTF_8)); 279 List<PluginInformation> pis = new PluginListParser().parse(in); 280 availablePlugins.addAll(filterDeprecatedPlugins(pis)); 281 } catch (PluginListParseException e) { 282 Main.error(tr("Failed to parse plugin list document from site ''{0}''. Skipping site. Exception was: {1}", site, e.toString())); 283 Main.error(e); 284 } 285 } 286 287 @Override 288 protected void realRun() throws SAXException, IOException, OsmTransferException { 289 if (sites == null) return; 290 getProgressMonitor().setTicksCount(sites.size() * 3); 291 292 // collect old cache files and remove if no longer in use 293 List<File> siteCacheFiles = new LinkedList<>(); 294 for (String location : PluginInformation.getPluginLocations()) { 295 File[] f = new File(location).listFiles( 296 new FilenameFilter() { 297 @Override 298 public boolean accept(File dir, String name) { 299 return name.matches("^([0-9]+-)?site.*\\.txt$") || 300 name.matches("^([0-9]+-)?site.*-icons\\.zip$"); 301 } 302 } 303 ); 304 if (f != null && f.length > 0) { 305 siteCacheFiles.addAll(Arrays.asList(f)); 306 } 307 } 308 309 File pluginDir = Main.pref.getPluginsDirectory(); 310 for (String site: sites) { 311 String printsite = site.replaceAll("%<(.*)>", ""); 312 getProgressMonitor().subTask(tr("Processing plugin list from site ''{0}''", printsite)); 313 String list = downloadPluginList(site, getProgressMonitor().createSubTaskMonitor(0, false)); 314 if (canceled) return; 315 siteCacheFiles.remove(createSiteCacheFile(pluginDir, site)); 316 if (list != null) { 317 getProgressMonitor().worked(1); 318 cachePluginList(site, list); 319 if (canceled) return; 320 getProgressMonitor().worked(1); 321 parsePluginListDocument(site, list); 322 if (canceled) return; 323 getProgressMonitor().worked(1); 324 if (canceled) return; 325 } 326 } 327 // remove old stuff or whole update process is broken 328 for (File file: siteCacheFiles) { 329 Utils.deleteFile(file); 330 } 331 } 332 333 /** 334 * Replies true if the task was canceled 335 * @return <code>true</code> if the task was stopped by the user 336 */ 337 public boolean isCanceled() { 338 return canceled; 339 } 340 341 /** 342 * Replies the list of plugins described in the downloaded plugin lists 343 * 344 * @return the list of plugins 345 * @since 5601 346 */ 347 public List<PluginInformation> getAvailablePlugins() { 348 return availablePlugins; 349 } 350}