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    }