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}