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