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.net.HttpURLConnection; 020import java.net.MalformedURLException; 021import java.net.URL; 022import java.nio.charset.StandardCharsets; 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 private boolean displayErrMsg; 059 060 protected final void init(Collection<String> sites, boolean displayErrMsg) { 061 this.sites = sites; 062 if (sites == null) { 063 this.sites = Collections.emptySet(); 064 } 065 this.availablePlugins = new LinkedList<>(); 066 this.displayErrMsg = displayErrMsg; 067 } 068 069 /** 070 * Constructs a new {@code ReadRemotePluginInformationTask}. 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, true); 077 } 078 079 /** 080 * Constructs a new {@code ReadRemotePluginInformationTask}. 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 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 085 */ 086 public ReadRemotePluginInformationTask(ProgressMonitor monitor, Collection<String> sites, boolean displayErrMsg) { 087 super(tr("Download plugin list..."), monitor == null ? NullProgressMonitor.INSTANCE: monitor, false /* don't ignore exceptions */); 088 init(sites, displayErrMsg); 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 file. 106 * 107 * @param site the name of the site 108 * @param type icon cache or plugin list cache 109 * @return the file name for the cache file 110 */ 111 protected File createSiteCacheFile(File pluginDir, String site) { 112 String name; 113 try { 114 site = site.replaceAll("%<(.*)>", ""); 115 URL url = new URL(site); 116 StringBuilder sb = new StringBuilder(); 117 sb.append("site-"); 118 sb.append(url.getHost()).append("-"); 119 if (url.getPort() != -1) { 120 sb.append(url.getPort()).append("-"); 121 } 122 String path = url.getPath(); 123 for (int i =0;i<path.length(); i++) { 124 char c = path.charAt(i); 125 if (Character.isLetterOrDigit(c)) { 126 sb.append(c); 127 } else { 128 sb.append("_"); 129 } 130 } 131 sb.append(".txt"); 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, final ProgressMonitor monitor) { 147 /* replace %<x> with empty string or x=plugins (separated with comma) */ 148 String pl = Utils.join(",", Main.pref.getCollection("plugins")); 149 String printsite = site.replaceAll("%<(.*)>", ""); 150 if (pl != null && pl.length() != 0) { 151 site = site.replaceAll("%<(.*)>", "$1"+pl); 152 } else { 153 site = printsite; 154 } 155 156 try { 157 monitor.beginTask(""); 158 monitor.indeterminateSubTask(tr("Downloading plugin list from ''{0}''", printsite)); 159 160 URL url = new URL(site); 161 synchronized(this) { 162 connection = Utils.openHttpConnection(url); 163 connection.setRequestProperty("Cache-Control", "no-cache"); 164 connection.setRequestProperty("Accept-Charset", "utf-8"); 165 } 166 try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { 167 StringBuilder sb = new StringBuilder(); 168 String line; 169 while ((line = in.readLine()) != null) { 170 sb.append(line).append("\n"); 171 } 172 return sb.toString(); 173 } 174 } catch (MalformedURLException e) { 175 if (canceled) return null; 176 Main.error(e); 177 return null; 178 } catch (IOException e) { 179 if (canceled) return null; 180 Main.addNetworkError(site, e); 181 handleIOException(monitor, e, tr("Plugin list download error"), tr("JOSM failed to download plugin list:"), displayErrMsg); 182 return null; 183 } finally { 184 synchronized(this) { 185 if (connection != null) { 186 connection.disconnect(); 187 } 188 connection = null; 189 } 190 monitor.finishTask(); 191 } 192 } 193 194 private void handleIOException(final ProgressMonitor monitor, IOException e, final String title, final String firstMessage, boolean displayMsg) { 195 StringBuilder sb = new StringBuilder(); 196 try (InputStream errStream = connection.getErrorStream()) { 197 if (errStream != null) { 198 try (BufferedReader err = new BufferedReader(new InputStreamReader(errStream, StandardCharsets.UTF_8))) { 199 String line; 200 while ((line = err.readLine()) != null) { 201 sb.append(line).append("\n"); 202 } 203 } catch (Exception ex) { 204 Main.error(e); 205 Main.error(ex); 206 } 207 } 208 } catch (IOException ex) { 209 Main.warn(ex); 210 } 211 final String msg = e.getMessage(); 212 final String details = sb.toString(); 213 if (details.isEmpty()) { 214 Main.error(e.getClass().getSimpleName()+": " + msg); 215 } else { 216 Main.error(msg + " - Details:\n" + details); 217 } 218 219 if (displayMsg) { 220 displayErrorMessage(monitor, msg, details, title, firstMessage); 221 } 222 } 223 224 private void displayErrorMessage(final ProgressMonitor monitor, final String msg, final String details, final String title, final String firstMessage) { 225 GuiHelper.runInEDTAndWait(new Runnable() { 226 @Override public void run() { 227 JPanel panel = new JPanel(new GridBagLayout()); 228 panel.add(new JLabel(firstMessage), GBC.eol().insets(0, 0, 0, 10)); 229 StringBuilder b = new StringBuilder(); 230 for (String part : msg.split("(?<=\\G.{200})")) { 231 b.append(part).append("\n"); 232 } 233 panel.add(new JLabel("<html><body width=\"500\"><b>"+b.toString().trim()+"</b></body></html>"), GBC.eol().insets(0, 0, 0, 10)); 234 if (!details.isEmpty()) { 235 panel.add(new JLabel(tr("Details:")), GBC.eol().insets(0, 0, 0, 10)); 236 JosmTextArea area = new JosmTextArea(details); 237 area.setEditable(false); 238 area.setLineWrap(true); 239 area.setWrapStyleWord(true); 240 JScrollPane scrollPane = new JScrollPane(area); 241 scrollPane.setPreferredSize(new Dimension(500, 300)); 242 panel.add(scrollPane, GBC.eol().fill()); 243 } 244 JOptionPane.showMessageDialog(monitor.getWindowParent(), panel, title, JOptionPane.ERROR_MESSAGE); 245 } 246 }); 247 } 248 249 /** 250 * Writes the list of plugins to a cache file 251 * 252 * @param site the site from where the list was downloaded 253 * @param list the downloaded list 254 */ 255 protected void cachePluginList(String site, String list) { 256 File pluginDir = Main.pref.getPluginsDirectory(); 257 if (!pluginDir.exists() && !pluginDir.mkdirs()) { 258 Main.warn(tr("Failed to create plugin directory ''{0}''. Cannot cache plugin list from plugin site ''{1}''.", pluginDir.toString(), site)); 259 } 260 File cacheFile = createSiteCacheFile(pluginDir, site); 261 getProgressMonitor().subTask(tr("Writing plugin list to local cache ''{0}''", cacheFile.toString())); 262 try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(cacheFile), StandardCharsets.UTF_8))) { 263 writer.write(list); 264 writer.flush(); 265 } catch(IOException e) { 266 // just failed to write the cache file. No big deal, but log the exception anyway 267 Main.error(e); 268 } 269 } 270 271 /** 272 * Filter information about deprecated plugins from the list of downloaded 273 * plugins 274 * 275 * @param plugins the plugin informations 276 * @return the plugin informations, without deprecated plugins 277 */ 278 protected List<PluginInformation> filterDeprecatedPlugins(List<PluginInformation> plugins) { 279 List<PluginInformation> ret = new ArrayList<>(plugins.size()); 280 HashSet<String> deprecatedPluginNames = new HashSet<>(); 281 for (PluginHandler.DeprecatedPlugin p : PluginHandler.DEPRECATED_PLUGINS) { 282 deprecatedPluginNames.add(p.name); 283 } 284 for (PluginInformation plugin: plugins) { 285 if (deprecatedPluginNames.contains(plugin.name)) { 286 continue; 287 } 288 ret.add(plugin); 289 } 290 return ret; 291 } 292 293 /** 294 * Parses the plugin list 295 * 296 * @param site the site from where the list was downloaded 297 * @param doc the document with the plugin list 298 */ 299 protected void parsePluginListDocument(String site, String doc) { 300 try { 301 getProgressMonitor().subTask(tr("Parsing plugin list from site ''{0}''", site)); 302 InputStream in = new ByteArrayInputStream(doc.getBytes(StandardCharsets.UTF_8)); 303 List<PluginInformation> pis = new PluginListParser().parse(in); 304 availablePlugins.addAll(filterDeprecatedPlugins(pis)); 305 } catch (PluginListParseException e) { 306 Main.error(tr("Failed to parse plugin list document from site ''{0}''. Skipping site. Exception was: {1}", site, e.toString())); 307 Main.error(e); 308 } 309 } 310 311 @Override 312 protected void realRun() throws SAXException, IOException, OsmTransferException { 313 if (sites == null) return; 314 getProgressMonitor().setTicksCount(sites.size() * 3); 315 File pluginDir = Main.pref.getPluginsDirectory(); 316 317 // collect old cache files and remove if no longer in use 318 List<File> siteCacheFiles = new LinkedList<>(); 319 for (String location : PluginInformation.getPluginLocations()) { 320 File [] f = new File(location).listFiles( 321 new FilenameFilter() { 322 @Override 323 public boolean accept(File dir, String name) { 324 return name.matches("^([0-9]+-)?site.*\\.txt$") || 325 name.matches("^([0-9]+-)?site.*-icons\\.zip$"); 326 } 327 } 328 ); 329 if(f != null && f.length > 0) { 330 siteCacheFiles.addAll(Arrays.asList(f)); 331 } 332 } 333 334 for (String site: sites) { 335 String printsite = site.replaceAll("%<(.*)>", ""); 336 getProgressMonitor().subTask(tr("Processing plugin list from site ''{0}''", printsite)); 337 String list = downloadPluginList(site, getProgressMonitor().createSubTaskMonitor(0, false)); 338 if (canceled) return; 339 siteCacheFiles.remove(createSiteCacheFile(pluginDir, site)); 340 if (list != null) { 341 getProgressMonitor().worked(1); 342 cachePluginList(site, list); 343 if (canceled) return; 344 getProgressMonitor().worked(1); 345 parsePluginListDocument(site, list); 346 if (canceled) return; 347 getProgressMonitor().worked(1); 348 if (canceled) return; 349 } 350 } 351 // remove old stuff or whole update process is broken 352 for (File file: siteCacheFiles) { 353 file.delete(); 354 } 355 } 356 357 /** 358 * Replies true if the task was canceled 359 * @return <code>true</code> if the task was stopped by the user 360 */ 361 public boolean isCanceled() { 362 return canceled; 363 } 364 365 /** 366 * Replies the list of plugins described in the downloaded plugin lists 367 * 368 * @return the list of plugins 369 * @since 5601 370 */ 371 public List<PluginInformation> getAvailablePlugins() { 372 return availablePlugins; 373 } 374}