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