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 }