source: josm/trunk/src/org/openstreetmap/josm/plugins/ReadRemotePluginInformationTask.java @ 12794

Last change on this file since 12794 was 12794, checked in by Don-vip, 3 months ago

fix #15274 - Support URLs with other protocol than http or https for plugin sites (patch by floscher)

  • Property svn:eol-style set to native
File size: 13.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.plugins;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Dimension;
7import java.awt.GridBagLayout;
8import java.io.ByteArrayInputStream;
9import java.io.File;
10import java.io.FilenameFilter;
11import java.io.IOException;
12import java.io.InputStream;
13import java.io.InputStreamReader;
14import java.io.PrintWriter;
15import java.net.MalformedURLException;
16import java.net.URL;
17import java.nio.charset.StandardCharsets;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.HashSet;
23import java.util.LinkedList;
24import java.util.List;
25import java.util.Optional;
26import java.util.Set;
27
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31import javax.swing.JScrollPane;
32
33import org.openstreetmap.josm.Main;
34import org.openstreetmap.josm.gui.PleaseWaitRunnable;
35import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
36import org.openstreetmap.josm.gui.progress.ProgressMonitor;
37import org.openstreetmap.josm.gui.util.GuiHelper;
38import org.openstreetmap.josm.gui.widgets.JosmTextArea;
39import org.openstreetmap.josm.io.OsmTransferException;
40import org.openstreetmap.josm.tools.GBC;
41import org.openstreetmap.josm.tools.HttpClient;
42import org.openstreetmap.josm.tools.Logging;
43import org.openstreetmap.josm.tools.Utils;
44import org.xml.sax.SAXException;
45
46/**
47 * An asynchronous task for downloading plugin lists from the configured plugin download sites.
48 * @since 2817
49 */
50public class ReadRemotePluginInformationTask extends PleaseWaitRunnable {
51
52    private Collection<String> sites;
53    private boolean canceled;
54    private HttpClient connection;
55    private List<PluginInformation> availablePlugins;
56    private boolean displayErrMsg;
57
58    protected final void init(Collection<String> sites, boolean displayErrMsg) {
59        this.sites = Optional.ofNullable(sites).orElseGet(Collections::emptySet);
60        this.availablePlugins = new LinkedList<>();
61        this.displayErrMsg = displayErrMsg;
62    }
63
64    /**
65     * Constructs a new {@code ReadRemotePluginInformationTask}.
66     *
67     * @param sites the collection of download sites. Defaults to the empty collection if null.
68     */
69    public ReadRemotePluginInformationTask(Collection<String> sites) {
70        super(tr("Download plugin list..."), false /* don't ignore exceptions */);
71        init(sites, true);
72    }
73
74    /**
75     * Constructs a new {@code ReadRemotePluginInformationTask}.
76     *
77     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
78     * @param sites the collection of download sites. Defaults to the empty collection if null.
79     * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
80     */
81    public ReadRemotePluginInformationTask(ProgressMonitor monitor, Collection<String> sites, boolean displayErrMsg) {
82        super(tr("Download plugin list..."), monitor == null ? NullProgressMonitor.INSTANCE : monitor, false /* don't ignore exceptions */);
83        init(sites, displayErrMsg);
84    }
85
86    @Override
87    protected void cancel() {
88        canceled = true;
89        synchronized (this) {
90            if (connection != null) {
91                connection.disconnect();
92            }
93        }
94    }
95
96    @Override
97    protected void finish() {
98        // Do nothing
99    }
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            final URL url = new URL(site);
159            if ("https".equals(url.getProtocol()) || "http".equals(url.getProtocol())) {
160                connection = HttpClient.create(url).useCache(false);
161                final HttpClient.Response response = connection.connect();
162                content = response.fetchContent();
163                if (response.getResponseCode() != 200) {
164                    throw new IOException(tr("Unsuccessful HTTP request"));
165                }
166                return content;
167            } else {
168                // e.g. when downloading from a file:// URL, we can't use HttpClient
169                try (InputStreamReader in = new InputStreamReader(url.openConnection().getInputStream(), StandardCharsets.UTF_8)) {
170                    final StringBuilder sb = new StringBuilder();
171                    final char[] buffer = new char[8192];
172                    int numChars;
173                    while ((numChars = in.read(buffer)) >= 0) {
174                        sb.append(buffer, 0, numChars);
175                        if (canceled) {
176                            return null;
177                        }
178                    }
179                    return sb.toString();
180                }
181            }
182
183        } catch (MalformedURLException e) {
184            if (canceled) return null;
185            Logging.error(e);
186            return null;
187        } catch (IOException e) {
188            if (canceled) return null;
189            handleIOException(monitor, e, content);
190            return null;
191        } finally {
192            synchronized (this) {
193                if (connection != null) {
194                    connection.disconnect();
195                }
196                connection = null;
197            }
198            monitor.finishTask();
199        }
200    }
201
202    private void handleIOException(final ProgressMonitor monitor, IOException e, String details) {
203        final String msg = e.getMessage();
204        if (details == null || details.isEmpty()) {
205            Logging.error(e.getClass().getSimpleName()+": " + msg);
206        } else {
207            Logging.error(msg + " - Details:\n" + details);
208        }
209
210        if (displayErrMsg) {
211            displayErrorMessage(monitor, msg, details, tr("Plugin list download error"), tr("JOSM failed to download plugin list:"));
212        }
213    }
214
215    private static void displayErrorMessage(final ProgressMonitor monitor, final String msg, final String details, final String title,
216            final String firstMessage) {
217        GuiHelper.runInEDTAndWait(() -> {
218            JPanel panel = new JPanel(new GridBagLayout());
219            panel.add(new JLabel(firstMessage), GBC.eol().insets(0, 0, 0, 10));
220            StringBuilder b = new StringBuilder();
221            for (String part : msg.split("(?<=\\G.{200})")) {
222                b.append(part).append('\n');
223            }
224            panel.add(new JLabel("<html><body width=\"500\"><b>"+b.toString().trim()+"</b></body></html>"), GBC.eol().insets(0, 0, 0, 10));
225            if (details != null && !details.isEmpty()) {
226                panel.add(new JLabel(tr("Details:")), GBC.eol().insets(0, 0, 0, 10));
227                JosmTextArea area = new JosmTextArea(details);
228                area.setEditable(false);
229                area.setLineWrap(true);
230                area.setWrapStyleWord(true);
231                JScrollPane scrollPane = new JScrollPane(area);
232                scrollPane.setPreferredSize(new Dimension(500, 300));
233                panel.add(scrollPane, GBC.eol().fill());
234            }
235            JOptionPane.showMessageDialog(monitor.getWindowParent(), panel, title, JOptionPane.ERROR_MESSAGE);
236        });
237    }
238
239    /**
240     * Writes the list of plugins to a cache file
241     *
242     * @param site the site from where the list was downloaded
243     * @param list the downloaded list
244     */
245    protected void cachePluginList(String site, String list) {
246        File pluginDir = Main.pref.getPluginsDirectory();
247        if (!pluginDir.exists() && !pluginDir.mkdirs()) {
248            Logging.warn(tr("Failed to create plugin directory ''{0}''. Cannot cache plugin list from plugin site ''{1}''.",
249                    pluginDir.toString(), site));
250        }
251        File cacheFile = createSiteCacheFile(pluginDir, site);
252        getProgressMonitor().subTask(tr("Writing plugin list to local cache ''{0}''", cacheFile.toString()));
253        try (PrintWriter writer = new PrintWriter(cacheFile, StandardCharsets.UTF_8.name())) {
254            writer.write(list);
255            writer.flush();
256        } catch (IOException e) {
257            // just failed to write the cache file. No big deal, but log the exception anyway
258            Logging.error(e);
259        }
260    }
261
262    /**
263     * Filter information about deprecated plugins from the list of downloaded
264     * plugins
265     *
266     * @param plugins the plugin informations
267     * @return the plugin informations, without deprecated plugins
268     */
269    protected List<PluginInformation> filterDeprecatedPlugins(List<PluginInformation> plugins) {
270        List<PluginInformation> ret = new ArrayList<>(plugins.size());
271        Set<String> deprecatedPluginNames = new HashSet<>();
272        for (PluginHandler.DeprecatedPlugin p : PluginHandler.DEPRECATED_PLUGINS) {
273            deprecatedPluginNames.add(p.name);
274        }
275        for (PluginInformation plugin: plugins) {
276            if (deprecatedPluginNames.contains(plugin.name)) {
277                continue;
278            }
279            ret.add(plugin);
280        }
281        return ret;
282    }
283
284    /**
285     * Parses the plugin list
286     *
287     * @param site the site from where the list was downloaded
288     * @param doc the document with the plugin list
289     */
290    protected void parsePluginListDocument(String site, String doc) {
291        try {
292            getProgressMonitor().subTask(tr("Parsing plugin list from site ''{0}''", site));
293            InputStream in = new ByteArrayInputStream(doc.getBytes(StandardCharsets.UTF_8));
294            List<PluginInformation> pis = new PluginListParser().parse(in);
295            availablePlugins.addAll(filterDeprecatedPlugins(pis));
296        } catch (PluginListParseException e) {
297            Logging.error(tr("Failed to parse plugin list document from site ''{0}''. Skipping site. Exception was: {1}", site, e.toString()));
298            Logging.error(e);
299        }
300    }
301
302    @Override
303    protected void realRun() throws SAXException, IOException, OsmTransferException {
304        if (sites == null) return;
305        getProgressMonitor().setTicksCount(sites.size() * 3);
306
307        // collect old cache files and remove if no longer in use
308        List<File> siteCacheFiles = new LinkedList<>();
309        for (String location : PluginInformation.getPluginLocations()) {
310            File[] f = new File(location).listFiles(
311                    (FilenameFilter) (dir, name) -> name.matches("^([0-9]+-)?site.*\\.txt$") ||
312                                                    name.matches("^([0-9]+-)?site.*-icons\\.zip$")
313            );
314            if (f != null && f.length > 0) {
315                siteCacheFiles.addAll(Arrays.asList(f));
316            }
317        }
318
319        File pluginDir = Main.pref.getPluginsDirectory();
320        for (String site: sites) {
321            String printsite = site.replaceAll("%<(.*)>", "");
322            getProgressMonitor().subTask(tr("Processing plugin list from site ''{0}''", printsite));
323            String list = downloadPluginList(site, getProgressMonitor().createSubTaskMonitor(0, false));
324            if (canceled) return;
325            siteCacheFiles.remove(createSiteCacheFile(pluginDir, site));
326            if (list != null) {
327                getProgressMonitor().worked(1);
328                cachePluginList(site, list);
329                if (canceled) return;
330                getProgressMonitor().worked(1);
331                parsePluginListDocument(site, list);
332                if (canceled) return;
333                getProgressMonitor().worked(1);
334                if (canceled) return;
335            }
336        }
337        // remove old stuff or whole update process is broken
338        for (File file: siteCacheFiles) {
339            Utils.deleteFile(file);
340        }
341    }
342
343    /**
344     * Replies true if the task was canceled
345     * @return <code>true</code> if the task was stopped by the user
346     */
347    public boolean isCanceled() {
348        return canceled;
349    }
350
351    /**
352     * Replies the list of plugins described in the downloaded plugin lists
353     *
354     * @return  the list of plugins
355     * @since 5601
356     */
357    public List<PluginInformation> getAvailablePlugins() {
358        return availablePlugins;
359    }
360}
Note: See TracBrowser for help on using the repository browser.