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.BufferedReader; 009import java.io.ByteArrayInputStream; 010import java.io.File; 011import java.io.FileOutputStream; 012import java.io.FilenameFilter; 013import java.io.IOException; 014import java.io.InputStream; 015import java.io.InputStreamReader; 016import java.io.OutputStream; 017import java.io.OutputStreamWriter; 018import java.io.PrintWriter; 019import java.io.UnsupportedEncodingException; 020import java.net.HttpURLConnection; 021import java.net.MalformedURLException; 022import java.net.URL; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.LinkedList; 029import java.util.List; 030 031import javax.swing.JLabel; 032import javax.swing.JOptionPane; 033import javax.swing.JPanel; 034import javax.swing.JScrollPane; 035 036import org.openstreetmap.josm.Main; 037import org.openstreetmap.josm.gui.PleaseWaitRunnable; 038import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 039import org.openstreetmap.josm.gui.progress.ProgressMonitor; 040import org.openstreetmap.josm.gui.util.GuiHelper; 041import org.openstreetmap.josm.gui.widgets.JosmTextArea; 042import org.openstreetmap.josm.io.OsmTransferException; 043import org.openstreetmap.josm.tools.GBC; 044import org.openstreetmap.josm.tools.ImageProvider; 045import org.openstreetmap.josm.tools.Utils; 046import org.xml.sax.SAXException; 047 048/** 049 * An asynchronous task for downloading plugin lists from the configured plugin download sites. 050 * @since 2817 051 */ 052public class ReadRemotePluginInformationTask extends PleaseWaitRunnable { 053 054 private Collection<String> sites; 055 private boolean canceled; 056 private HttpURLConnection connection; 057 private List<PluginInformation> availablePlugins; 058 059 protected enum CacheType {PLUGIN_LIST, ICON_LIST} 060 061 protected void init(Collection<String> sites){ 062 this.sites = sites; 063 if (sites == null) { 064 this.sites = Collections.emptySet(); 065 } 066 availablePlugins = new LinkedList<PluginInformation>(); 067 068 } 069 /** 070 * Creates the task 071 * 072 * @param sites the collection of download sites. Defaults to the empty collection if null. 073 */ 074 public ReadRemotePluginInformationTask(Collection<String> sites) { 075 super(tr("Download plugin list..."), false /* don't ignore exceptions */); 076 init(sites); 077 } 078 079 /** 080 * Creates the task 081 * 082 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 083 * @param sites the collection of download sites. Defaults to the empty collection if null. 084 */ 085 public ReadRemotePluginInformationTask(ProgressMonitor monitor, Collection<String> sites) { 086 super(tr("Download plugin list..."), monitor == null ? NullProgressMonitor.INSTANCE: monitor, false /* don't ignore exceptions */); 087 init(sites); 088 } 089 090 091 @Override 092 protected void cancel() { 093 canceled = true; 094 synchronized(this) { 095 if (connection != null) { 096 connection.disconnect(); 097 } 098 } 099 } 100 101 @Override 102 protected void finish() {} 103 104 /** 105 * Creates the file name for the cached plugin list and the icon cache 106 * file. 107 * 108 * @param site the name of the site 109 * @param type icon cache or plugin list cache 110 * @return the file name for the cache file 111 */ 112 protected File createSiteCacheFile(File pluginDir, String site, CacheType type) { 113 String name; 114 try { 115 site = site.replaceAll("%<(.*)>", ""); 116 URL url = new URL(site); 117 StringBuilder sb = new StringBuilder(); 118 sb.append("site-"); 119 sb.append(url.getHost()).append("-"); 120 if (url.getPort() != -1) { 121 sb.append(url.getPort()).append("-"); 122 } 123 String path = url.getPath(); 124 for (int i =0;i<path.length(); i++) { 125 char c = path.charAt(i); 126 if (Character.isLetterOrDigit(c)) { 127 sb.append(c); 128 } else { 129 sb.append("_"); 130 } 131 } 132 switch (type) { 133 case PLUGIN_LIST: 134 sb.append(".txt"); 135 break; 136 case ICON_LIST: 137 sb.append("-icons.zip"); 138 break; 139 } 140 name = sb.toString(); 141 } catch(MalformedURLException e) { 142 name = "site-unknown.txt"; 143 } 144 return new File(pluginDir, name); 145 } 146 147 /** 148 * Downloads the list from a remote location 149 * 150 * @param site the site URL 151 * @param monitor a progress monitor 152 * @return the downloaded list 153 */ 154 protected String downloadPluginList(String site, final ProgressMonitor monitor) { 155 BufferedReader in = null; 156 String line; 157 try { 158 /* replace %<x> with empty string or x=plugins (separated with comma) */ 159 String pl = Utils.join(",", Main.pref.getCollection("plugins")); 160 String printsite = site.replaceAll("%<(.*)>", ""); 161 if(pl != null && pl.length() != 0) { 162 site = site.replaceAll("%<(.*)>", "$1"+pl); 163 } else { 164 site = printsite; 165 } 166 167 monitor.beginTask(""); 168 monitor.indeterminateSubTask(tr("Downloading plugin list from ''{0}''", printsite)); 169 170 URL url = new URL(site); 171 synchronized(this) { 172 connection = Utils.openHttpConnection(url); 173 connection.setRequestProperty("Cache-Control", "no-cache"); 174 connection.setRequestProperty("Accept-Charset", "utf-8"); 175 } 176 in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); 177 StringBuilder sb = new StringBuilder(); 178 while ((line = in.readLine()) != null) { 179 sb.append(line).append("\n"); 180 } 181 return sb.toString(); 182 } catch (MalformedURLException e) { 183 if (canceled) return null; 184 e.printStackTrace(); 185 return null; 186 } catch (IOException e) { 187 if (canceled) return null; 188 handleIOException(monitor, e, tr("Plugin list download error"), tr("JOSM failed to download plugin list:")); 189 return null; 190 } finally { 191 synchronized(this) { 192 if (connection != null) { 193 connection.disconnect(); 194 } 195 connection = null; 196 } 197 Utils.close(in); 198 monitor.finishTask(); 199 } 200 } 201 202 private void handleIOException(final ProgressMonitor monitor, IOException e, final String title, String firstMessage) { 203 InputStream errStream = connection.getErrorStream(); 204 StringBuilder sb = new StringBuilder(); 205 if (errStream != null) { 206 BufferedReader err = null; 207 try { 208 String line; 209 err = new BufferedReader(new InputStreamReader(errStream, "UTF-8")); 210 while ((line = err.readLine()) != null) { 211 sb.append(line).append("\n"); 212 } 213 } catch (Exception ex) { 214 Main.error(e); 215 Main.error(ex); 216 } finally { 217 Utils.close(err); 218 } 219 } 220 final String msg = e.getMessage(); 221 final String details = sb.toString(); 222 Main.error(details.isEmpty() ? msg : msg + " - Details:\n" + details); 223 224 GuiHelper.runInEDTAndWait(new Runnable() { 225 @Override public void run() { 226 JPanel panel = new JPanel(new GridBagLayout()); 227 panel.add(new JLabel(tr("JOSM failed to download plugin list:")), GBC.eol().insets(0, 0, 0, 10)); 228 StringBuilder b = new StringBuilder(); 229 for (String part : msg.split("(?<=\\G.{200})")) { 230 b.append(part).append("\n"); 231 } 232 panel.add(new JLabel("<html><body width=\"500\"><b>"+b.toString().trim()+"</b></body></html>"), GBC.eol().insets(0, 0, 0, 10)); 233 if (!details.isEmpty()) { 234 panel.add(new JLabel(tr("Details:")), GBC.eol().insets(0, 0, 0, 10)); 235 JosmTextArea area = new JosmTextArea(details); 236 area.setEditable(false); 237 area.setLineWrap(true); 238 area.setWrapStyleWord(true); 239 JScrollPane scrollPane = new JScrollPane(area); 240 scrollPane.setPreferredSize(new Dimension(500, 300)); 241 panel.add(scrollPane, GBC.eol().fill()); 242 } 243 JOptionPane.showMessageDialog(monitor.getWindowParent(), panel, title, JOptionPane.ERROR_MESSAGE); 244 } 245 }); 246 } 247 248 /** 249 * Downloads the icon archive from a remote location 250 * 251 * @param site the site URL 252 * @param monitor a progress monitor 253 */ 254 protected void downloadPluginIcons(String site, File destFile, ProgressMonitor monitor) { 255 InputStream in = null; 256 OutputStream out = null; 257 try { 258 site = site.replaceAll("%<(.*)>", ""); 259 260 monitor.beginTask(""); 261 monitor.indeterminateSubTask(tr("Downloading plugin list from ''{0}''", site)); 262 263 URL url = new URL(site); 264 synchronized(this) { 265 connection = Utils.openHttpConnection(url); 266 connection.setRequestProperty("Cache-Control", "no-cache"); 267 } 268 in = connection.getInputStream(); 269 out = new FileOutputStream(destFile); 270 byte[] buffer = new byte[8192]; 271 for (int read = in.read(buffer); read != -1; read = in.read(buffer)) { 272 out.write(buffer, 0, read); 273 } 274 } catch (MalformedURLException e) { 275 if (canceled) return; 276 e.printStackTrace(); 277 return; 278 } catch (IOException e) { 279 if (canceled) return; 280 handleIOException(monitor, e, tr("Plugin icons download error"), tr("JOSM failed to download plugin icons:")); 281 return; 282 } finally { 283 Utils.close(out); 284 synchronized(this) { 285 if (connection != null) { 286 connection.disconnect(); 287 } 288 connection = null; 289 } 290 Utils.close(in); 291 monitor.finishTask(); 292 } 293 for (PluginInformation pi : availablePlugins) { 294 if (pi.icon == null && pi.iconPath != null) { 295 pi.icon = new ImageProvider(pi.name+".jar/"+pi.iconPath) 296 .setArchive(destFile) 297 .setMaxWidth(24) 298 .setMaxHeight(24) 299 .setOptional(true).get(); 300 } 301 } 302 } 303 304 /** 305 * Writes the list of plugins to a cache file 306 * 307 * @param site the site from where the list was downloaded 308 * @param list the downloaded list 309 */ 310 protected void cachePluginList(String site, String list) { 311 PrintWriter writer = null; 312 try { 313 File pluginDir = Main.pref.getPluginsDirectory(); 314 if (!pluginDir.exists()) { 315 if (! pluginDir.mkdirs()) { 316 Main.warn(tr("Failed to create plugin directory ''{0}''. Cannot cache plugin list from plugin site ''{1}''.", pluginDir.toString(), site)); 317 } 318 } 319 File cacheFile = createSiteCacheFile(pluginDir, site, CacheType.PLUGIN_LIST); 320 getProgressMonitor().subTask(tr("Writing plugin list to local cache ''{0}''", cacheFile.toString())); 321 writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(cacheFile), "utf-8")); 322 writer.write(list); 323 } catch(IOException e) { 324 // just failed to write the cache file. No big deal, but log the exception anyway 325 e.printStackTrace(); 326 } finally { 327 if (writer != null) { 328 writer.flush(); 329 Utils.close(writer); 330 } 331 } 332 } 333 334 /** 335 * Filter information about deprecated plugins from the list of downloaded 336 * plugins 337 * 338 * @param plugins the plugin informations 339 * @return the plugin informations, without deprecated plugins 340 */ 341 protected List<PluginInformation> filterDeprecatedPlugins(List<PluginInformation> plugins) { 342 List<PluginInformation> ret = new ArrayList<PluginInformation>(plugins.size()); 343 HashSet<String> deprecatedPluginNames = new HashSet<String>(); 344 for (PluginHandler.DeprecatedPlugin p : PluginHandler.DEPRECATED_PLUGINS) { 345 deprecatedPluginNames.add(p.name); 346 } 347 for (PluginInformation plugin: plugins) { 348 if (deprecatedPluginNames.contains(plugin.name)) { 349 continue; 350 } 351 ret.add(plugin); 352 } 353 return ret; 354 } 355 356 /** 357 * Parses the plugin list 358 * 359 * @param site the site from where the list was downloaded 360 * @param doc the document with the plugin list 361 */ 362 protected void parsePluginListDocument(String site, String doc) { 363 try { 364 getProgressMonitor().subTask(tr("Parsing plugin list from site ''{0}''", site)); 365 InputStream in = new ByteArrayInputStream(doc.getBytes("UTF-8")); 366 List<PluginInformation> pis = new PluginListParser().parse(in); 367 availablePlugins.addAll(filterDeprecatedPlugins(pis)); 368 } catch (UnsupportedEncodingException e) { 369 Main.error(tr("Failed to parse plugin list document from site ''{0}''. Skipping site. Exception was: {1}", site, e.toString())); 370 e.printStackTrace(); 371 } catch (PluginListParseException e) { 372 Main.error(tr("Failed to parse plugin list document from site ''{0}''. Skipping site. Exception was: {1}", site, e.toString())); 373 e.printStackTrace(); 374 } 375 } 376 377 @Override 378 protected void realRun() throws SAXException, IOException, OsmTransferException { 379 if (sites == null) return; 380 getProgressMonitor().setTicksCount(sites.size() * 3); 381 File pluginDir = Main.pref.getPluginsDirectory(); 382 383 // collect old cache files and remove if no longer in use 384 List<File> siteCacheFiles = new LinkedList<File>(); 385 for (String location : PluginInformation.getPluginLocations()) { 386 File [] f = new File(location).listFiles( 387 new FilenameFilter() { 388 @Override 389 public boolean accept(File dir, String name) { 390 return name.matches("^([0-9]+-)?site.*\\.txt$") || 391 name.matches("^([0-9]+-)?site.*-icons\\.zip$"); 392 } 393 } 394 ); 395 if(f != null && f.length > 0) { 396 siteCacheFiles.addAll(Arrays.asList(f)); 397 } 398 } 399 400 for (String site: sites) { 401 String printsite = site.replaceAll("%<(.*)>", ""); 402 getProgressMonitor().subTask(tr("Processing plugin list from site ''{0}''", printsite)); 403 String list = downloadPluginList(site, getProgressMonitor().createSubTaskMonitor(0, false)); 404 if (canceled) return; 405 siteCacheFiles.remove(createSiteCacheFile(pluginDir, site, CacheType.PLUGIN_LIST)); 406 siteCacheFiles.remove(createSiteCacheFile(pluginDir, site, CacheType.ICON_LIST)); 407 if(list != null) 408 { 409 getProgressMonitor().worked(1); 410 cachePluginList(site, list); 411 if (canceled) return; 412 getProgressMonitor().worked(1); 413 parsePluginListDocument(site, list); 414 if (canceled) return; 415 getProgressMonitor().worked(1); 416 if (canceled) return; 417 } 418 downloadPluginIcons(site+"-icons.zip", createSiteCacheFile(pluginDir, site, CacheType.ICON_LIST), getProgressMonitor().createSubTaskMonitor(0, false)); 419 } 420 for (File file: siteCacheFiles) /* remove old stuff or whole update process is broken */ 421 { 422 file.delete(); 423 } 424 } 425 426 /** 427 * Replies true if the task was canceled 428 * @return <code>true</code> if the task was stopped by the user 429 */ 430 public boolean isCanceled() { 431 return canceled; 432 } 433 434 /** 435 * Replies the list of plugins described in the downloaded plugin lists 436 * 437 * @return the list of plugins 438 * @since 5601 439 */ 440 public List<PluginInformation> getAvailablePlugins() { 441 return availablePlugins; 442 } 443}