Index: trunk/src/org/openstreetmap/josm/Main.java
===================================================================
--- trunk/src/org/openstreetmap/josm/Main.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/Main.java	(revision 2817)
@@ -47,5 +47,4 @@
 import org.openstreetmap.josm.gui.MainMenu;
 import org.openstreetmap.josm.gui.MapFrame;
-import org.openstreetmap.josm.gui.SplashScreen;
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.io.SaveLayersDialog;
@@ -174,5 +173,5 @@
         Main.map = map;
 
-        PluginHandler.setMapFrame(old, map);
+        PluginHandler.notifyMapFrameChanged(old, map);
     }
 
@@ -192,8 +191,4 @@
 
     public Main() {
-        this(null);
-    }
-
-    public Main(SplashScreen splash) {
         main = this;
         //        platform = determinePlatformHook();
@@ -201,8 +196,4 @@
         contentPane.add(panel, BorderLayout.CENTER);
         panel.add(gettingStarted, BorderLayout.CENTER);
-
-        if(splash != null) {
-            splash.setStatus(tr("Creating main GUI"));
-        }
         menu = new MainMenu();
 
Index: trunk/src/org/openstreetmap/josm/data/Preferences.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 2817)
@@ -202,5 +202,5 @@
     }
 
-    public File getPluginsDirFile() {
+    public File getPluginsDirectory() {
         return new File(getPreferencesDirFile(), "plugins");
     }
@@ -748,3 +748,27 @@
         System.setProperties(sysProp);
     }
+
+    /**
+     * The default plugin site
+     */
+    private final static String[] DEFAULT_PLUGIN_SITE = {"http://josm.openstreetmap.de/plugin"};
+
+    /**
+     * Replies the collection of plugin site URLs from where plugin lists can be downloaded
+     * 
+     * @return
+     */
+    public Collection<String> getPluginSites() {
+        return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE));
+    }
+
+    /**
+     * Sets the collection of plugin site URLs.
+     * 
+     * @param sites the site URLs
+     */
+    public void setPluginSites(Collection<String> sites) {
+        putCollection("pluginmanager.sites", sites);
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/gui/MainApplication.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 2817)
@@ -22,8 +22,10 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
 import org.openstreetmap.josm.io.DefaultProxySelector;
 import org.openstreetmap.josm.io.auth.CredentialsManagerFactory;
 import org.openstreetmap.josm.io.auth.DefaultAuthenticator;
 import org.openstreetmap.josm.plugins.PluginHandler;
+import org.openstreetmap.josm.plugins.PluginInformation;
 import org.openstreetmap.josm.tools.BugReportExceptionHandler;
 import org.openstreetmap.josm.tools.I18n;
@@ -45,6 +47,6 @@
      * display the frame.
      */
-    public MainApplication(JFrame mainFrame, SplashScreen splash) {
-        super(splash);
+    public MainApplication(JFrame mainFrame) {
+        super();
         mainFrame.setContentPane(contentPane);
         mainFrame.setJMenuBar(menu);
@@ -151,25 +153,43 @@
         }
 
-        SplashScreen splash = new SplashScreen(Main.pref.getBoolean("draw.splashscreen", true));
-
-        splash.setStatus(tr("Activating updated plugins"));
-        PluginHandler.earlyCleanup();
-
-        splash.setStatus(tr("Loading early plugins"));
-        PluginHandler.loadPlugins(true);
-
-        splash.setStatus(tr("Setting defaults"));
+        SplashScreen splash = new SplashScreen();
+        ProgressMonitor monitor = splash.getProgressMonitor();
+        monitor.beginTask(tr("Initializing"));
+        monitor.setTicksCount(7);
+        splash.setVisible(Main.pref.getBoolean("draw.splashscreen", true));
+
+        List<PluginInformation> pluginsToLoad = PluginHandler.buildListOfPluginsToLoad(monitor.createSubTaskMonitor(1, false));
+        if (!pluginsToLoad.isEmpty() && PluginHandler.checkAndConfirmPluginUpdate()) {
+            monitor.subTask(tr("Updating plugins..."));
+            PluginHandler.updatePlugins(pluginsToLoad, monitor.createSubTaskMonitor(1, false));
+        }
+        monitor.worked(1);
+
+        monitor.subTask(tr("Installing updated plugins"));
+        PluginHandler.installDownloadedPlugins();
+        monitor.worked(1);
+
+        monitor.subTask(tr("Loading early plugins"));
+        PluginHandler.loadEarlyPlugins(pluginsToLoad, monitor.createSubTaskMonitor(1, false));
+        monitor.worked(1);
+
+        monitor.subTask(tr("Setting defaults"));
         preConstructorInit(args);
         removeObsoletePreferences();
-        splash.setStatus(tr("Creating main GUI"));
+        monitor.worked(1);
+
+        monitor.indeterminateSubTask(tr("Creating main GUI"));
         JFrame mainFrame = new JFrame(tr("Java OpenStreetMap Editor"));
         Main.parent = mainFrame;
-        final Main main = new MainApplication(mainFrame, splash);
-        splash.setStatus(tr("Loading plugins"));
-        PluginHandler.loadPlugins(false);
+        final Main main = new MainApplication(mainFrame);
+        monitor.worked(1);
+
+        monitor.subTask(tr("Loading plugins"));
+        PluginHandler.loadLatePlugins(pluginsToLoad,  monitor.createSubTaskMonitor(1, false));
+        monitor.worked(1);
         toolbar.refreshToolbarControl();
-
+        splash.setVisible(false);
+        splash.dispose();
         mainFrame.setVisible(true);
-        splash.closeSplash();
 
         if (((!args.containsKey("no-maximize") && !args.containsKey("geometry")
Index: trunk/src/org/openstreetmap/josm/gui/SplashScreen.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/SplashScreen.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/gui/SplashScreen.java	(revision 2817)
@@ -4,5 +4,4 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
-import java.awt.AWTEvent;
 import java.awt.Color;
 import java.awt.Dimension;
@@ -12,5 +11,4 @@
 import java.awt.Insets;
 import java.awt.Toolkit;
-import java.awt.event.AWTEventListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
@@ -18,13 +16,15 @@
 import javax.swing.JLabel;
 import javax.swing.JPanel;
+import javax.swing.JProgressBar;
 import javax.swing.JSeparator;
 import javax.swing.JWindow;
-import javax.swing.SwingUtilities;
 import javax.swing.border.Border;
 import javax.swing.border.EmptyBorder;
 import javax.swing.border.EtchedBorder;
 
-import org.openstreetmap.josm.actions.AboutAction;
 import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressRenderer;
+import org.openstreetmap.josm.gui.progress.SwingRenderingProgressMonitor;
 import org.openstreetmap.josm.tools.ImageProvider;
 
@@ -32,19 +32,12 @@
  * Show a splash screen so the user knows what is happening during startup.
  *
- * @author cbrill
  */
 public class SplashScreen extends JWindow {
 
-    private JLabel status;
-    private boolean visible;
+    private SplashScreenProgressRenderer progressRenderer;
+    private SwingRenderingProgressMonitor progressMonitor;
 
-    private Runnable closerRunner;
-
-    public SplashScreen(boolean visible) {
+    public SplashScreen() {
         super();
-        this.visible=visible;
-
-        if (!visible)
-            return;
 
         // Add a nice border to the main splash screen
@@ -89,9 +82,9 @@
 
         // Add a status message
-        status = new JLabel();
+        progressRenderer = new SplashScreenProgressRenderer();
         gbc.gridy = 3;
-        gbc.insets = new Insets(0, 0, 0, 0);
-        innerContentPane.add(status, gbc);
-        setStatus(tr("Initializing"));
+        gbc.insets = new Insets(5, 5, 10, 5);
+        innerContentPane.add(progressRenderer, gbc);
+        progressMonitor = new SwingRenderingProgressMonitor(progressRenderer);
 
         pack();
@@ -103,65 +96,77 @@
                 screenSize.height / 2 - (labelSize.height / 2));
 
-        // Method to close the splash screen when being clicked or when closeSplash is called
-        closerRunner = new Runnable() {
-            public void run() {
-                setVisible(false);
-                dispose();
-            }
-        };
-
         // Add ability to hide splash screen by clicking it
         addMouseListener(new MouseAdapter() {
+            @Override
             public void mousePressed(MouseEvent event) {
-                try {
-                    closerRunner.run();
-                } catch (Exception e) {
-                    e.printStackTrace();
-                    // can catch InvocationTargetException
-                    // can catch InterruptedException
-                }
+                setVisible(false);
             }
         });
-
-        // Hide splashscreen when other window is created
-        Toolkit.getDefaultToolkit().addAWTEventListener(awtListener, AWTEvent.WINDOW_EVENT_MASK);
-
-        setVisible(true);
     }
 
-    private AWTEventListener awtListener = new AWTEventListener() {
-        public void eventDispatched(AWTEvent event) {
-            if (event.getSource() != SplashScreen.this) {
-                closeSplash();
-            }
-        }
-    };
-
-    /**
-     * This method sets the status message. It should be called prior to
-     * actually doing the action.
-     *
-     * @param message
-     *            the message to be displayed
-     */
-    public void setStatus(String message) {
-        if (!visible)
-            return;
-        status.setText(message + "...");
+    public ProgressMonitor getProgressMonitor() {
+        return progressMonitor;
     }
 
-    /**
-     * Closes the splashscreen. Call once you are done starting.
-     */
-    public void closeSplash() {
-        if (!visible)
-            return;
-        Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener);
-        try {
-            SwingUtilities.invokeLater(closerRunner);
-        } catch (Exception e) {
-            e.printStackTrace();
-            // can catch InvocationTargetException
-            // can catch InterruptedException
+    static private class SplashScreenProgressRenderer extends JPanel implements ProgressRenderer {
+        private JLabel lblTaskTitle;
+        private JLabel lblCustomText;
+        private JProgressBar progressBar;
+
+        protected void build() {
+            setLayout(new GridBagLayout());
+            GridBagConstraints gc = new GridBagConstraints();
+            gc.gridx = 0;
+            gc.gridy = 0;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            gc.weightx = 1.0;
+            gc.weighty = 0.0;
+            gc.insets = new Insets(5,0,0,5);
+            add(lblTaskTitle = new JLabel(""), gc);
+
+            gc.gridx = 0;
+            gc.gridy = 1;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            gc.weightx = 1.0;
+            gc.weighty = 0.0;
+            gc.insets = new Insets(5,0,0,5);
+            add(lblCustomText = new JLabel(""), gc);
+
+            gc.gridx = 0;
+            gc.gridy = 2;
+            gc.fill = GridBagConstraints.HORIZONTAL;
+            gc.weightx = 1.0;
+            gc.weighty = 0.0;
+            gc.insets = new Insets(5,0,0,5);
+            add(progressBar = new JProgressBar(JProgressBar.HORIZONTAL), gc);
+        }
+
+        public SplashScreenProgressRenderer() {
+            build();
+        }
+
+        public void setCustomText(String message) {
+            lblCustomText.setText(message);
+            repaint();
+        }
+
+        public void setIndeterminate(boolean indeterminate) {
+            progressBar.setIndeterminate(indeterminate);
+            repaint();
+        }
+
+        public void setMaximum(int maximum) {
+            progressBar.setMaximum(maximum);
+            repaint();
+        }
+
+        public void setTaskTitle(String taskTitle) {
+            lblTaskTitle.setText(taskTitle);
+            repaint();
+        }
+
+        public void setValue(int value) {
+            progressBar.setValue(value);
+            repaint();
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/preferences/PluginPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/PluginPreference.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/PluginPreference.java	(revision 2817)
@@ -3,32 +3,53 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.awt.Dimension;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
-import java.awt.Rectangle;
+import java.awt.Insets;
 import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
 import java.util.Collection;
 import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Logger;
 
 import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
 import javax.swing.DefaultListModel;
 import javax.swing.JButton;
 import javax.swing.JLabel;
 import javax.swing.JList;
-import javax.swing.JTextField;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
-import javax.swing.Scrollable;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.plugins.PluginDownloader;
-import org.openstreetmap.josm.plugins.PluginSelection;
+import org.openstreetmap.josm.gui.HelpAwareOptionPane;
+import org.openstreetmap.josm.gui.help.HelpUtil;
+import org.openstreetmap.josm.gui.preferences.plugin.PluginPreferencesModel;
+import org.openstreetmap.josm.gui.preferences.plugin.PluginPreferencesPanel;
+import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
+import org.openstreetmap.josm.plugins.PluginDownloadTask;
+import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
+import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
 import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
 
 public class PluginPreference implements PreferenceSetting {
+    private final static Logger logger = Logger.getLogger(PluginPreference.class.getName());
 
     public static class Factory implements PreferenceSettingFactory {
@@ -38,73 +59,105 @@
     }
 
-    private JPanel plugin;
-    private JPanel pluginPanel = new NoHorizontalScrollPanel(new GridBagLayout());
-    private PreferenceTabbedPane gui;
-    private JScrollPane pluginPane;
-    private PluginSelection selection = new PluginSelection();
-    private JTextField txtFilter;
+    public static String buildDownloadSummary(PluginDownloadTask task) {
+        Collection<PluginInformation> downloaded = task.getDownloadedPlugins();
+        Collection<PluginInformation> failed = task.getFailedPlugins();
+        StringBuilder sb = new StringBuilder();
+        if (! downloaded.isEmpty()) {
+            sb.append(trn(
+                    "The following plugin has been downloaded <strong>successfully</strong>:",
+                    "The following {0} plugins have been downloaded successfully:",
+                    downloaded.size(),
+                    downloaded.size()
+            ));
+            sb.append("<ul>");
+            for(PluginInformation pi: downloaded) {
+                sb.append("<li>").append(pi.name).append("</li>");
+            }
+            sb.append("</ul>");
+        }
+        if (! failed.isEmpty()) {
+            sb.append(trn(
+                    "Downloading the following plugin has <strong>failed</strong>:",
+                    "Downloading the following {0} plugins has <strong>failed</strong>:",
+                    failed.size(),
+                    failed.size()
+            ));
+            sb.append("<ul>");
+            for(PluginInformation pi: failed) {
+                sb.append("<li>").append(pi.name).append("</li>");
+            }
+            sb.append("</ul>");
+        }
+        return sb.toString();
+    }
+
+    private JTextField tfFilter;
+    private PluginPreferencesPanel pnlPluginPreferences;
+    private PluginPreferencesModel model;
+    private JScrollPane spPluginPreferences;
+
+    protected JPanel buildSearchFieldPanel() {
+        JPanel pnl  = new JPanel(new GridBagLayout());
+        pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+        GridBagConstraints gc = new GridBagConstraints();
+
+        gc.anchor = GridBagConstraints.NORTHWEST;
+        gc.fill = GridBagConstraints.HORIZONTAL;
+        gc.weightx = 0.0;
+        gc.insets = new Insets(0,0,0,3);
+        pnl.add(new JLabel(tr("Search:")), gc);
+
+        gc.gridx = 1;
+        gc.weightx = 1.0;
+        pnl.add(tfFilter = new JTextField(), gc);
+        tfFilter.setToolTipText(tr("Enter a search expression"));
+        SelectAllOnFocusGainedDecorator.decorate(tfFilter);
+        tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter());
+        return pnl;
+    }
+
+    protected JPanel buildActionPanel() {
+        JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
+
+        pnl.add(new JButton(new DownloadAvailablePluginsAction()));
+        pnl.add(new JButton(new UpdateSelectedPluginsAction()));
+        pnl.add(new JButton(new ConfigureSitesAction()));
+        return pnl;
+    }
+
+    protected JPanel buildContentPane() {
+        JPanel pnl = new JPanel(new BorderLayout());
+        pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH);
+        model  = new PluginPreferencesModel();
+        spPluginPreferences = new JScrollPane(pnlPluginPreferences = new PluginPreferencesPanel(model));
+        spPluginPreferences.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        spPluginPreferences.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+        spPluginPreferences.getVerticalScrollBar().addComponentListener(
+                new ComponentAdapter(){
+                    @Override
+                    public void componentShown(ComponentEvent e) {
+                        spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border"));
+                    }
+                    @Override
+                    public void componentHidden(ComponentEvent e) {
+                        spPluginPreferences.setBorder(null);
+                    }
+                }
+        );
+
+        pnl.add(spPluginPreferences, BorderLayout.CENTER);
+        pnl.add(buildActionPanel(), BorderLayout.SOUTH);
+        return pnl;
+    }
 
     public void addGui(final PreferenceTabbedPane gui) {
-        this.gui = gui;
-        plugin = gui.createPreferenceTab("plugin", tr("Plugins"), tr("Configure available plugins."), false);
-
-        txtFilter = new JTextField();
-        JLabel lbFilter = new JLabel(tr("Search: "));
-        lbFilter.setLabelFor(txtFilter);
-        plugin.add(lbFilter);
-        plugin.add(txtFilter, GBC.eol().fill(GBC.HORIZONTAL));
-        txtFilter.getDocument().addDocumentListener(new DocumentListener(){
-            public void changedUpdate(DocumentEvent e) {
-                action();
-            }
-
-            public void insertUpdate(DocumentEvent e) {
-                action();
-            }
-
-            public void removeUpdate(DocumentEvent e) {
-                action();
-            }
-
-            private void action() {
-                selection.drawPanel(pluginPanel);
-            }
-        });
-        plugin.add(GBC.glue(0,10), GBC.eol());
-
-        /* main plugin area */
-        pluginPane = new JScrollPane(pluginPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
-        pluginPane.setBorder(null);
-        plugin.add(pluginPane, GBC.eol().fill(GBC.BOTH));
-        plugin.add(GBC.glue(0,10), GBC.eol());
-
-        /* buttons at the bottom */
-        JButton morePlugins = new JButton(tr("Download List"));
-        morePlugins.addActionListener(new ActionListener(){
-            public void actionPerformed(ActionEvent e) {
-                selection.updateDescription(pluginPanel);
-            }
-        });
-        plugin.add(morePlugins, GBC.std().insets(0,0,10,0));
-
-        JButton update = new JButton(tr("Update"));
-        update.addActionListener(new ActionListener(){
-            public void actionPerformed(ActionEvent e) {
-                selection.update(pluginPanel);
-            }
-        });
-        plugin.add(update, GBC.std().insets(0,0,10,0));
-
-        JButton configureSites = new JButton(tr("Configure Sites..."));
-        configureSites.addActionListener(new ActionListener(){
-            public void actionPerformed(ActionEvent e) {
-                configureSites();
-            }
-        });
-        plugin.add(configureSites, GBC.std());
-
-        selection.passTxtFilter(txtFilter);
-        selection.loadPlugins();
-        selection.drawPanel(pluginPanel);
+        GridBagConstraints gc = new GridBagConstraints();
+        gc.weightx = 1.0;
+        gc.weighty = 1.0;
+        gc.anchor = GridBagConstraints.NORTHWEST;
+        gc.fill = GridBagConstraints.BOTH;
+        gui.plugins.add(buildContentPane(), gc);
+        pnlPluginPreferences.refreshView();
+        gui.addChangeListener(new PluginPreferenceActivationListener(gui.plugins));
     }
 
@@ -113,5 +166,5 @@
         p.add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol());
         final DefaultListModel model = new DefaultListModel();
-        for (String s : PluginDownloader.getSites()) {
+        for (String s : Main.pref.getPluginSites()) {
             model.addElement(s);
         }
@@ -122,5 +175,5 @@
             public void actionPerformed(ActionEvent e) {
                 String s = JOptionPane.showInputDialog(
-                        gui,
+                        JOptionPane.getFrameForComponent(pnlPluginPreferences),
                         tr("Add JOSM Plugin description URL."),
                         tr("Enter URL"),
@@ -136,5 +189,5 @@
                 if (list.getSelectedValue() == null) {
                     JOptionPane.showMessageDialog(
-                            gui,
+                            JOptionPane.getFrameForComponent(pnlPluginPreferences),
                             tr("Please select an entry."),
                             tr("Warning"),
@@ -159,5 +212,5 @@
                 if (list.getSelectedValue() == null) {
                     JOptionPane.showMessageDialog(
-                            gui,
+                            JOptionPane.getFrameForComponent(pnlPluginPreferences),
                             tr("Please select an entry."),
                             tr("Warning"),
@@ -171,5 +224,5 @@
         p.add(buttons, GBC.eol());
         int answer = JOptionPane.showConfirmDialog(
-                gui,
+                JOptionPane.getFrameForComponent(pnlPluginPreferences),
                 p,
                 tr("Configure Plugin Sites"), JOptionPane.OK_CANCEL_OPTION,
@@ -181,34 +234,183 @@
             sites.add((String)model.getElementAt(i));
         }
-        PluginDownloader.setSites(sites);
+        Main.pref.setPluginSites(sites);
+    }
+
+    /**
+     * Replies the list of plugins waiting for update or download
+     * 
+     * @return the list of plugins waiting for update or download
+     */
+    public List<PluginInformation> getPluginsScheduledForUpdateOrDownload() {
+        return model.getPluginsScheduledForUpdateOrDownload();
     }
 
     public boolean ok() {
-        return selection.finish();
-    }
-
-    private static class NoHorizontalScrollPanel extends JPanel implements Scrollable {
-        public NoHorizontalScrollPanel(GridBagLayout gridBagLayout) {
-            super(gridBagLayout);
-        }
-
-        public Dimension getPreferredScrollableViewportSize() {
-            return super.getPreferredSize();
-        }
-
-        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
-            return 30;
-        }
-
-        public boolean getScrollableTracksViewportHeight() {
-            return false;
-        }
-
-        public boolean getScrollableTracksViewportWidth() {
+        if (model.isActivePluginsChanged()) {
+            Main.pref.putCollection("plugins", model.getSelectedPluginNames());
             return true;
         }
-
-        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
-            return 10;
+        return false;
+    }
+
+    public void readLocalPluginInformation() {
+        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
+        Runnable r = new Runnable() {
+            public void run() {
+                if (task.isCanceled()) return;
+                SwingUtilities.invokeLater(new Runnable() {
+                    public void run() {
+                        model.setAvailablePlugins(task.getAvailablePlugins());
+                        pnlPluginPreferences.refreshView();
+                    }
+                });
+            };
+        };
+        Main.worker.submit(task);
+        Main.worker.submit(r);
+    }
+
+    /**
+     * The action for downloading the list of available plugins
+     *
+     */
+    class DownloadAvailablePluginsAction extends AbstractAction {
+
+        public DownloadAvailablePluginsAction() {
+            putValue(NAME,tr("Download list"));
+            putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins"));
+            putValue(SMALL_ICON, ImageProvider.get("download"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(Main.pref.getPluginSites());
+            Runnable continuation = new Runnable() {
+                public void run() {
+                    if (task.isCanceled()) return;
+                    SwingUtilities.invokeLater(new Runnable() {
+                        public void run() {
+                            model.setAvailablePlugins(task.getAvailabePlugins());
+                            pnlPluginPreferences.refreshView();
+
+                        }
+                    });
+                }
+            };
+            Main.worker.submit(task);
+            Main.worker.submit(continuation);
+        }
+    }
+
+    /**
+     * The action for downloading the list of available plugins
+     *
+     */
+    class UpdateSelectedPluginsAction extends AbstractAction {
+        public UpdateSelectedPluginsAction() {
+            putValue(NAME,tr("Update plugins"));
+            putValue(SHORT_DESCRIPTION, tr("Update the selected plugins"));
+            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
+        }
+
+        protected void notifyDownloadResults(PluginDownloadTask task) {
+            Collection<PluginInformation> downloaded = task.getDownloadedPlugins();
+            Collection<PluginInformation> failed = task.getFailedPlugins();
+            StringBuilder sb = new StringBuilder();
+            sb.append("<html>");
+            sb.append(buildDownloadSummary(task));
+            if (!downloaded.isEmpty()) {
+                sb.append("Please restart JOSM to activate the downloaded plugins.");
+            }
+            sb.append("</html>");
+            HelpAwareOptionPane.showOptionDialog(
+                    pnlPluginPreferences,
+                    sb.toString(),
+                    tr("Update plugins"),
+                    failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE,
+                            // FIXME: check help topic
+                            HelpUtil.ht("/Preferences/Plugin")
+            );
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            List<PluginInformation> toUpdate = model.getSelectedPlugins();
+            final PluginDownloadTask task = new PluginDownloadTask(
+                    pnlPluginPreferences,
+                    toUpdate,
+                    tr("Update plugins")
+            );
+            Runnable r = new Runnable() {
+                public void run() {
+                    if (task.isCanceled())
+                        return;
+                    notifyDownloadResults(task);
+                    model.refreshLocalPluginVersion(task.getDownloadedPlugins());
+                    pnlPluginPreferences.refreshView();
+                }
+            };
+            Main.worker.submit(task);
+            Main.worker.submit(r);
+        }
+    }
+
+
+    /**
+     * The action for configuring the plugin download sites
+     *
+     */
+    class ConfigureSitesAction extends AbstractAction {
+        public ConfigureSitesAction() {
+            putValue(NAME,tr("Configure sites..."));
+            putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from"));
+        }
+
+        public void actionPerformed(ActionEvent e) {
+            configureSites();
+        }
+    }
+
+    /**
+     * Listens to the activation of the plugin preferences tab. On activation it
+     * reloads plugin information from the local file system.
+     *
+     */
+    class PluginPreferenceActivationListener implements ChangeListener {
+        private Component pane;
+        public PluginPreferenceActivationListener(Component preferencesPane) {
+            pane = preferencesPane;
+        }
+
+        public void stateChanged(ChangeEvent e) {
+            JTabbedPane tp = (JTabbedPane)e.getSource();
+            if (tp.getSelectedComponent() == pane) {
+                readLocalPluginInformation();
+            }
+        }
+    }
+
+    /**
+     * Applies the current filter condition in the filter text field to the
+     * model
+     */
+    class SearchFieldAdapter implements DocumentListener {
+        public void filter() {
+            String expr = tfFilter.getText().trim();
+            if (expr.equals("")) {
+                expr = null;
+            }
+            model.filterDisplayedPlugins(expr);
+            pnlPluginPreferences.refreshView();
+        }
+
+        public void changedUpdate(DocumentEvent arg0) {
+            filter();
+        }
+
+        public void insertUpdate(DocumentEvent arg0) {
+            filter();
+        }
+
+        public void removeUpdate(DocumentEvent arg0) {
+            filter();
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(revision 2817)
@@ -23,7 +23,10 @@
 import javax.swing.JScrollPane;
 import javax.swing.JTabbedPane;
+import javax.swing.SwingUtilities;
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.PluginDownloadTask;
 import org.openstreetmap.josm.plugins.PluginHandler;
+import org.openstreetmap.josm.plugins.PluginInformation;
 import org.openstreetmap.josm.tools.BugReportExceptionHandler;
 import org.openstreetmap.josm.tools.GBC;
@@ -47,4 +50,5 @@
     public final JPanel map = createPreferenceTab("map", I18n.tr("Map Settings"), I18n.tr("Settings for the map projection and data interpretation."));
     public final JPanel audio = createPreferenceTab("audio", I18n.tr("Audio Settings"), I18n.tr("Settings for the audio player and audio markers."));
+    public final JPanel plugins = createPreferenceTab("plugin", tr("Plugins"), tr("Configure available plugins."), false);
 
     public final javax.swing.JTabbedPane displaycontent = new javax.swing.JTabbedPane();
@@ -95,21 +99,89 @@
     }
 
+    protected PluginPreference getPluginPreference() {
+        for (PreferenceSetting setting: settings) {
+            if (setting instanceof PluginPreference)
+                return (PluginPreference) setting;
+        }
+        return null;
+    }
+
     public void savePreferences() {
-        boolean requiresRestart = false;
-        for (PreferenceSetting setting : settings)
-        {
-            if(setting.ok()) {
-                requiresRestart = true;
+
+        // create a task for downloading plugins if the user has activated, yet not downloaded,
+        // new plugins
+        //
+        final PluginPreference preference = getPluginPreference();
+        final List<PluginInformation> toDownload = preference.getPluginsScheduledForUpdateOrDownload();
+        final PluginDownloadTask task;
+        if (! toDownload.isEmpty()) {
+            task = new PluginDownloadTask(this, toDownload, tr("Download plugins"));
+        } else {
+            task = null;
+        }
+
+        // this is the task which will run *after* the plugins are downloaded
+        //
+        final Runnable continuation = new Runnable() {
+            public void run() {
+                boolean requiresRestart = false;
+                if (task != null && !task.isCanceled()) {
+                    if (!task.getDownloadedPlugins().isEmpty()) {
+                        requiresRestart = true;
+                    }
+                }
+
+                for (PreferenceSetting setting : settings) {
+                    if (setting.ok()) {
+                        requiresRestart = true;
+                    }
+                }
+
+                // build the messages. We only display one message, including the status
+                // information from the plugin download task and - if necessary - a hint
+                // to restart JOSM
+                //
+                StringBuffer sb = new StringBuffer();
+                sb.append("<html>");
+                if (task != null && !task.isCanceled()) {
+                    sb.append(PluginPreference.buildDownloadSummary(task));
+                }
+                if (requiresRestart) {
+                    sb.append(tr("You have to restart JOSM for some settings to take effect."));
+                }
+                sb.append("</html>");
+
+                // display the message, if necessary
+                //
+                if ((task != null && !task.isCanceled()) || requiresRestart) {
+                    JOptionPane.showMessageDialog(
+                            Main.parent,
+                            sb.toString(),
+                            tr("Warning"),
+                            JOptionPane.WARNING_MESSAGE
+                    );
+                }
+                Main.parent.repaint();
             }
-        }
-        if (requiresRestart) {
-            JOptionPane.showMessageDialog(
-                    Main.parent,
-                    tr("You have to restart JOSM for some settings to take effect."),
-                    tr("Warning"),
-                    JOptionPane.WARNING_MESSAGE
+        };
+
+        if (task != null) {
+            // if we have to launch a plugin download task we do it asynchronously, followed
+            // by the remaining "save preferences" activites run on the Swing EDT.
+            //
+            Main.worker.submit(task);
+            Main.worker.submit(
+                    new Runnable() {
+                        public void run() {
+                            SwingUtilities.invokeLater(continuation);
+                        }
+                    }
             );
-        }
-        Main.parent.repaint();
+        } else {
+            // no need for asynchronous activities. Simply run the remaining "save preference"
+            // activities on this thread (we are already on the Swing EDT
+            //
+            continuation.run();
+        }
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferencesModel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferencesModel.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferencesModel.java	(revision 2817)
@@ -0,0 +1,307 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.plugin;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Observable;
+import java.util.Set;
+import java.util.Map.Entry;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.PluginException;
+import org.openstreetmap.josm.plugins.PluginInformation;
+
+public class PluginPreferencesModel extends Observable{
+    private final static Logger logger = Logger.getLogger(PluginPreferencesModel.class.getName());
+
+    private final ArrayList<PluginInformation> availablePlugins = new ArrayList<PluginInformation>();
+    private final ArrayList<PluginInformation> displayedPlugins = new ArrayList<PluginInformation>();
+    private final HashMap<PluginInformation, Boolean> selectedPluginsMap = new HashMap<PluginInformation, Boolean>();
+    private Set<String> pendingDownloads = new HashSet<String>();
+    private String filterExpression;
+    private Set<String> currentActivePlugins;
+
+    public PluginPreferencesModel() {
+        currentActivePlugins = new HashSet<String>();
+        currentActivePlugins.addAll(Main.pref.getCollection("plugins", currentActivePlugins));
+    }
+
+    public void filterDisplayedPlugins(String filter) {
+        if (filter == null) {
+            displayedPlugins.clear();
+            displayedPlugins.addAll(availablePlugins);
+            this.filterExpression = filter;
+            return;
+        }
+        displayedPlugins.clear();
+        for (PluginInformation pi: availablePlugins) {
+            if (pi.matches(filter)) {
+                displayedPlugins.add(pi);
+            }
+        }
+        filterExpression = filter;
+        clearChanged();
+        notifyObservers();
+    }
+
+    public void setAvailablePlugins(Collection<PluginInformation> available) {
+        availablePlugins.clear();
+        if (available != null) {
+            availablePlugins.addAll(available);
+        }
+        sort();
+        filterDisplayedPlugins(filterExpression);
+        Set<String> activePlugins = new HashSet<String>();
+        activePlugins.addAll(Main.pref.getCollection("plugins", activePlugins));
+        for (PluginInformation pi: availablePlugins) {
+            if (selectedPluginsMap.get(pi) == null) {
+                if (activePlugins.contains(pi.name)) {
+                    selectedPluginsMap.put(pi, true);
+                }
+            }
+        }
+        clearChanged();
+        notifyObservers();
+    }
+
+    /**
+     * Replies the list of selected plugin information objects
+     * 
+     * @return the list of selected plugin information objects
+     */
+    public List<PluginInformation> getSelectedPlugins() {
+        List<PluginInformation> ret = new LinkedList<PluginInformation>();
+        for (PluginInformation pi: availablePlugins) {
+            if (selectedPluginsMap.get(pi) == null) {
+                continue;
+            }
+            if (selectedPluginsMap.get(pi)) {
+                ret.add(pi);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the list of selected plugin information objects
+     * 
+     * @return the list of selected plugin information objects
+     */
+    public Set<String> getSelectedPluginNames() {
+        Set<String> ret = new HashSet<String>();
+        for (PluginInformation pi: getSelectedPlugins()) {
+            ret.add(pi.name);
+        }
+        return ret;
+    }
+
+    /**
+     * Sorts the list of available plugins
+     */
+    protected void sort() {
+        Collections.sort(
+                availablePlugins,
+                new Comparator<PluginInformation>() {
+                    public int compare(PluginInformation o1, PluginInformation o2) {
+                        String n1 = o1.getName() == null ? "" : o1.getName();
+                        String n2 = o2.getName() == null ? "" : o2.getName();
+                        return n1.compareTo(n2);
+                    }
+                }
+        );
+    }
+
+    /**
+     * Replies the list of plugin informations to display
+     * 
+     * @return the list of plugin informations to display
+     */
+    public List<PluginInformation> getDisplayedPlugins() {
+        return displayedPlugins;
+    }
+
+
+    /**
+     * Replies the list of plugins waiting for update or download
+     * 
+     * @return the list of plugins waiting for update or download
+     */
+    public List<PluginInformation> getPluginsScheduledForUpdateOrDownload() {
+        List<PluginInformation> ret = new ArrayList<PluginInformation>();
+        for (String plugin: pendingDownloads) {
+            PluginInformation pi = getPluginInformation(plugin);
+            if (pi == null) {
+                continue;
+            }
+            ret.add(pi);
+        }
+        return ret;
+    }
+
+    /**
+     * Sets whether the plugin is selected or not.
+     * 
+     * @param name the name of the plugin
+     * @param selected true, if selected; false, otherwise
+     */
+    public void setPluginSelected(String name, boolean selected) {
+        PluginInformation pi = getPluginInformation(name);
+        if (pi != null) {
+            selectedPluginsMap.put(pi,selected);
+        }
+        if (!selected) {
+            pendingDownloads.remove(name);
+            return;
+        }
+        if (pi.isUpdateRequired()) {
+            pendingDownloads.add(pi.name);
+        }
+    }
+
+    /**
+     * Replies the plugin info with the name <code>name</code>. null, if no
+     * such plugin info exists.
+     * 
+     * @param name the name. If null, replies null.
+     * @return the plugin info.
+     */
+    public PluginInformation getPluginInformation(String name) {
+        for (PluginInformation pi: availablePlugins) {
+            if (pi.getName() != null && pi.getName().equals(name))
+                return pi;
+        }
+        return null;
+    }
+
+    /**
+     * Initializes the model from preferences
+     */
+    public void initFromPreferences() {
+        Collection<String> enabledPlugins = Main.pref.getCollection("plugins", null);
+        if (enabledPlugins == null) {
+            this.selectedPluginsMap.clear();
+            return;
+        }
+        for (String name: enabledPlugins) {
+            PluginInformation pi = getPluginInformation(name);
+            if (pi == null) {
+                continue;
+            }
+            setPluginSelected(name, true);
+        }
+    }
+
+    /**
+     * Replies true if the plugin with name <code>name</code> is currently
+     * selected in the plugin model
+     * 
+     * @param name the plugin name
+     * @return true if the plugin is selected; false, otherwise
+     */
+    public boolean isSelectedPlugin(String name) {
+        PluginInformation pi = getPluginInformation(name);
+        if (pi == null) return false;
+        if (selectedPluginsMap.get(pi) == null) return false;
+        return selectedPluginsMap.get(pi);
+    }
+
+    /**
+     * Replies the set of plugins which have been added by the user to
+     * the set of activated plugins.
+     * 
+     * @return the set of newly deactivated plugins
+     */
+    public List<PluginInformation> getNewlyActivatedPlugins() {
+        List<PluginInformation> ret = new LinkedList<PluginInformation>();
+        for (Entry<PluginInformation, Boolean> entry: selectedPluginsMap.entrySet()) {
+            PluginInformation pi = entry.getKey();
+            boolean selected = entry.getValue();
+            if (selected && ! currentActivePlugins.contains(pi.name)) {
+                ret.add(pi);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the set of plugins which have been removed by the user from
+     * the set of activated plugins.
+     * 
+     * @return the set of newly deactivated plugins
+     */
+    public List<PluginInformation> getNewlyDeactivatedPlugins() {
+        List<PluginInformation> ret = new LinkedList<PluginInformation>();
+        for (PluginInformation pi: availablePlugins) {
+            if (!currentActivePlugins.contains(pi.name)) {
+                continue;
+            }
+            if (selectedPluginsMap.get(pi) == null || ! selectedPluginsMap.get(pi)) {
+                ret.add(pi);
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Replies the set of plugin names which have been added by the user to
+     * the set of activated plugins.
+     * 
+     * @return the set of newly activated plugin names
+     */
+    public Set<String> getNewlyActivatedPluginNames() {
+        Set<String> ret = new HashSet<String>();
+        List<PluginInformation> plugins = getNewlyActivatedPlugins();
+        for (PluginInformation pi: plugins) {
+            ret.add(pi.name);
+        }
+        return ret;
+    }
+
+    /**
+     * Replies true if the set of active plugins has been changed by the user
+     * in this preference model. He has either added plugins or removed plugins
+     * being active before.
+     * 
+     * @return true if the collection of active plugins has changed
+     */
+    public boolean isActivePluginsChanged() {
+        Set<String> newActivePlugins = getSelectedPluginNames();
+        return ! newActivePlugins.equals(currentActivePlugins);
+    }
+
+    /**
+     * Refreshes the local version field on the plugins in <code>plugins</code> with
+     * the version in the manifest of the downloaded "jar.new"-file for this plugin.
+     * 
+     * @param plugins the collections of plugins to refresh
+     */
+    public void refreshLocalPluginVersion(Collection<PluginInformation> plugins) {
+        if (plugins == null) return;
+        File pluginDir = Main.pref.getPluginsDirectory();
+        for (PluginInformation pi : plugins) {
+            File downloadedPluginFile = new File(pluginDir, pi.name + ".jar.new");
+            if (! downloadedPluginFile.exists() && downloadedPluginFile.canRead()) {
+                continue;
+            }
+            try {
+                PluginInformation newinfo = new PluginInformation(downloadedPluginFile, pi.name);
+                PluginInformation oldinfo = getPluginInformation(pi.name);
+                if (oldinfo == null) {
+                    // should not happen
+                    continue;
+                }
+                oldinfo.localversion = newinfo.version;
+            } catch(PluginException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferencesPanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferencesPanel.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferencesPanel.java	(revision 2817)
@@ -0,0 +1,144 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.plugin;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+import javax.swing.event.HyperlinkEvent.EventType;
+
+import org.openstreetmap.josm.gui.JMultilineLabel;
+import org.openstreetmap.josm.gui.widgets.HtmlPanel;
+import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
+import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.tools.OpenBrowser;
+
+public class PluginPreferencesPanel extends VerticallyScrollablePanel{
+    private static final Logger logger = Logger.getLogger(PluginPreferencesPanel.class.getName());
+
+    private PluginPreferencesModel model;
+
+    public PluginPreferencesPanel() {
+        model = new PluginPreferencesModel();
+        setLayout(new GridBagLayout());
+    }
+
+    public PluginPreferencesPanel(PluginPreferencesModel model) {
+        this.model = model;
+        setLayout(new GridBagLayout());
+    }
+
+    protected String formatPluginRemoteVersion(PluginInformation pi) {
+        StringBuilder sb = new StringBuilder();
+        if (pi.version == null || pi.version.trim().equals("")) {
+            sb.append(tr("unknown"));
+        } else {
+            sb.append(pi.version);
+            if (pi.oldmode) {
+                sb.append("*");
+            }
+        }
+        return sb.toString();
+    }
+
+    protected String formatPluginLocalVersion(PluginInformation pi) {
+        if (pi == null) return tr("unknown");
+        if (pi.localversion == null || pi.localversion.trim().equals(""))
+            return tr("unknown");
+        return pi.localversion;
+    }
+
+    protected String formatCheckboxTooltipText(PluginInformation pi) {
+        if (pi == null) return "";
+        if (pi.downloadlink == null)
+            return tr("Plugin bundled with JOSM");
+        else
+            return pi.downloadlink;
+    }
+
+    public void displayEmptyPluginListInformation() {
+        GridBagConstraints gbc = new GridBagConstraints();
+        gbc.gridx = 0;
+        gbc.anchor = GridBagConstraints.CENTER;
+        gbc.fill = GridBagConstraints.BOTH;
+        gbc.insets = new Insets(40,0,40,0);
+        gbc.weightx = 1.0;
+        gbc.weighty = 1.0;
+
+        JMultilineLabel hint = new JMultilineLabel("");
+        hint.setFont(hint.getFont().deriveFont(Font.PLAIN));
+        hint.setHorizontalAlignment(JLabel.CENTER);
+        hint.setText(
+                "<html>"
+                + tr("Please click on <strong>Download list</strong> to download and display a list of available plugins.")
+                + "</html>"
+        );
+        add(hint, gbc);
+    }
+
+    public void refreshView() {
+        List<PluginInformation> displayedPlugins = model.getDisplayedPlugins();
+        removeAll();
+
+        GridBagConstraints gbc = new GridBagConstraints();
+        gbc.gridx = 0;
+        gbc.anchor = GridBagConstraints.NORTHWEST;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        gbc.weightx = 1.0;
+
+        if (displayedPlugins.isEmpty()) {
+            displayEmptyPluginListInformation();
+            return;
+        }
+
+        int row = -1;
+        for (final PluginInformation pi : displayedPlugins) {
+            boolean selected = model.isSelectedPlugin(pi.getName());
+            String remoteversion = formatPluginRemoteVersion(pi);
+            String localversion = formatPluginLocalVersion(model.getPluginInformation(pi.getName()));
+
+            final JCheckBox cbPlugin = new JCheckBox(
+                    tr("{0}: Version {1} (local: {2})", pi.getName(), remoteversion, localversion)
+            );
+            cbPlugin.setSelected(selected);
+            cbPlugin.setToolTipText(formatCheckboxTooltipText(pi));
+            cbPlugin.addActionListener(new ActionListener(){
+                public void actionPerformed(ActionEvent e) {
+                    model.setPluginSelected(pi.getName(), cbPlugin.isSelected());
+                }
+            });
+            gbc.gridy = ++row;
+            gbc.insets = new Insets(5,5,0,5);
+            gbc.weighty = 0.0;
+            add(cbPlugin, gbc);
+
+            HtmlPanel description = new HtmlPanel();
+            description.setText(pi.getDescriptionAsHtml());
+            description.getEditorPane().addHyperlinkListener(new HyperlinkListener() {
+                public void hyperlinkUpdate(HyperlinkEvent e) {
+                    if(e.getEventType() == EventType.ACTIVATED) {
+                        OpenBrowser.displayUrl(e.getURL().toString());
+                    }
+                }
+            });
+
+            gbc.gridy = ++row;
+            gbc.insets = new Insets(3,25,5,5);
+            gbc.weighty = 1.0;
+            add(description, gbc);
+        }
+        revalidate();
+        repaint();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/io/CacheFiles.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/CacheFiles.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/io/CacheFiles.java	(revision 2817)
@@ -59,5 +59,5 @@
 
     public CacheFiles(String ident, boolean isPlugin) {
-        String pref = isPlugin ? Main.pref.getPluginsDirFile().getPath() : Main.pref.getPreferencesDir();
+        String pref = isPlugin ? Main.pref.getPluginsDirectory().getPath() : Main.pref.getPreferencesDir();
 
         boolean dir_writeable;
Index: trunk/src/org/openstreetmap/josm/plugins/Plugin.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/Plugin.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/plugins/Plugin.java	(revision 2817)
@@ -53,5 +53,5 @@
      */
     public final String getPluginDir() {
-        return new File(Main.pref.getPluginsDirFile(), info.name).getPath();
+        return new File(Main.pref.getPluginsDirectory(), info.name).getPath();
     }
 
@@ -81,11 +81,13 @@
         String pluginDirName = Main.pref.getPreferencesDir()+"plugins/"+info.name+"/";
         File pluginDir = new File(pluginDirName);
-        if (!pluginDir.exists())
+        if (!pluginDir.exists()) {
             pluginDir.mkdirs();
+        }
         FileOutputStream out = new FileOutputStream(pluginDirName+to);
         InputStream in = getClass().getResourceAsStream(from);
         byte[] buffer = new byte[8192];
-        for(int len = in.read(buffer); len > 0; len = in.read(buffer))
+        for(int len = in.read(buffer); len > 0; len = in.read(buffer)) {
             out.write(buffer, 0, len);
+        }
         in.close();
         out.close();
Index: trunk/src/org/openstreetmap/josm/plugins/PluginDownloadException.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginDownloadException.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginDownloadException.java	(revision 2817)
@@ -0,0 +1,21 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins;
+
+public class PluginDownloadException extends Exception {
+
+    public PluginDownloadException() {
+        super();
+    }
+
+    public PluginDownloadException(String arg0, Throwable arg1) {
+        super(arg0, arg1);
+    }
+
+    public PluginDownloadException(String arg0) {
+        super(arg0);
+    }
+
+    public PluginDownloadException(Throwable arg0) {
+        super(arg0);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/plugins/PluginDownloadTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginDownloadTask.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginDownloadTask.java	(revision 2817)
@@ -0,0 +1,201 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.logging.Logger;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.xml.sax.SAXException;
+
+
+/**
+ * Asynchronous task for downloading a collection of plugins.
+ * 
+ * When the task is finished {@see #getDownloadedPlugins()} replies the list of downloaded plugins
+ * and {@see #getFailedPlugins()} replies the list of failed plugins.
+ * 
+ */
+public class PluginDownloadTask extends PleaseWaitRunnable{
+    private static final Logger logger = Logger.getLogger(PluginDownloadTask.class.getName());
+
+    private final Collection<PluginInformation> toUpdate;
+    private final Collection<PluginInformation> failed = new LinkedList<PluginInformation>();
+    private final Collection<PluginInformation> downloaded = new LinkedList<PluginInformation>();
+    private Exception lastException;
+    private boolean canceled;
+    private HttpURLConnection downloadConnection;
+
+    /**
+     * Creates the download task
+     * 
+     * @param parent the parent component relative to which the {@see PleaseWaitDialog} is displayed
+     * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null.
+     * @param title the title to display in the {@see PleaseWaitDialog}
+     * @throws IllegalArgumentException thrown if toUpdate is null
+     */
+    public PluginDownloadTask(Component parent, Collection<PluginInformation> toUpdate, String title) throws IllegalArgumentException{
+        super(parent, title == null ? "" : title, false /* don't ignore exceptions */);
+        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
+        this.toUpdate = toUpdate;
+    }
+
+    /**
+     * Creates the task
+     * 
+     * @param monitor a progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null
+     * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null.
+     * @param title the title to display in the {@see PleaseWaitDialog}
+     * @throws IllegalArgumentException thrown if toUpdate is null
+     */
+    public PluginDownloadTask(ProgressMonitor monitor, Collection<PluginInformation> toUpdate, String title) {
+        super(title, monitor == null? NullProgressMonitor.INSTANCE: monitor, false /* don't ignore exceptions */);
+        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
+        this.toUpdate = toUpdate;
+    }
+
+    @Override protected void cancel() {
+        this.canceled = true;
+        synchronized(this) {
+            if (downloadConnection != null) {
+                downloadConnection.disconnect();
+            }
+        }
+    }
+
+
+
+    @Override protected void finish() {}
+
+    protected void download(PluginInformation pi, File file) throws PluginDownloadException{
+        if (pi.mainversion > Version.getInstance().getVersion()) {
+            ExtendedDialog dialog = new ExtendedDialog(
+                    Main.parent,
+                    tr("Skip download"),
+                    new String[] {
+                        tr("Download Plugin"),
+                        tr("Skip Download") }
+            );
+            dialog.setContent(tr("JOSM version {0} required for plugin {1}.", pi.mainversion, pi.name));
+            dialog.setButtonIcons(new String[] { "download.png", "cancel.png" });
+            dialog.showDialog();
+            int answer = dialog.getValue();
+            if (answer != 1)
+                return;
+        }
+        OutputStream out = null;
+        InputStream in = null;
+        try {
+            if (pi.downloadlink == null) {
+                String msg = tr("Warning: Cannot download plugin ''{0}''. Its download link isn''t known. Skipping download.", pi.name);
+                System.err.println(msg);
+                throw new PluginDownloadException(msg);
+            }
+            URL url = new URL(pi.downloadlink);
+            synchronized(this) {
+                downloadConnection = (HttpURLConnection)url.openConnection();
+                downloadConnection.connect();
+            }
+            in = downloadConnection.getInputStream();
+            out = new FileOutputStream(file);
+            byte[] buffer = new byte[8192];
+            for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
+                out.write(buffer, 0, read);
+            }
+            out.close();
+            in.close();
+        } catch(MalformedURLException e) {
+            String msg = tr("Warning: Cannot download plugin ''{0}''. Its download link ''{1}'' isn''t a valid URL. Skipping download.", pi.name, pi.downloadlink);
+            System.err.println(msg);
+            throw new PluginDownloadException(msg);
+        } catch (IOException e) {
+            if (canceled)
+                return;
+            throw new PluginDownloadException(e);
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch(IOException e) { /* ignore */}
+            }
+            synchronized(this) {
+                downloadConnection = null;
+            }
+            if (out != null) {
+                try {
+                    out.close();
+                } catch(IOException e) { /* ignore */}
+            }
+        }
+    }
+
+    @Override protected void realRun() throws SAXException, IOException {
+        File pluginDir = Main.pref.getPluginsDirectory();
+        if (!pluginDir.exists()) {
+            if (!pluginDir.mkdirs()) {
+                lastException = new PluginDownloadException(tr("Failed to create plugin directory ''{0}''", pluginDir.toString()));
+                failed.addAll(toUpdate);
+                return;
+            }
+        }
+        getProgressMonitor().setTicksCount(toUpdate.size());
+        for (PluginInformation d : toUpdate) {
+            if (canceled) return;
+            progressMonitor.subTask(tr("Downloading Plugin {0}...", d.name));
+            progressMonitor.worked(1);
+            File pluginFile = new File(pluginDir, d.name + ".jar.new");
+            try {
+                download(d, pluginFile);
+            } catch(PluginDownloadException e) {
+                e.printStackTrace();
+                failed.add(d);
+                continue;
+            }
+            downloaded.add(d);
+        }
+    }
+
+    /**
+     * Replies true if the task was cancelled by the user
+     * 
+     * @return
+     */
+    public boolean isCanceled() {
+        return canceled;
+    }
+
+    /**
+     * Replies the list of successfully downloaded plugins
+     * 
+     * @return the list of successfully downloaded plugins
+     */
+    public Collection<PluginInformation> getFailedPlugins() {
+        return failed;
+    }
+
+    /**
+     * Replies the list of plugins whose download has failed
+     * 
+     * @return the list of plugins whose download has failed
+     */
+    public Collection<PluginInformation> getDownloadedPlugins() {
+        return downloaded;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/plugins/PluginDownloader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginDownloader.java	(revision 2815)
+++ 	(revision )
@@ -1,224 +1,0 @@
-//License: GPL. Copyright 2007 by Immanuel Scholz and others
-/**
- *
- */
-package org.openstreetmap.josm.plugins;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trn;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.net.URL;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.LinkedList;
-
-import javax.swing.JOptionPane;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.AboutAction;
-import org.openstreetmap.josm.data.Version;
-import org.openstreetmap.josm.gui.ExtendedDialog;
-import org.openstreetmap.josm.gui.PleaseWaitRunnable;
-import org.xml.sax.SAXException;
-
-public class PluginDownloader {
-
-    private static final class UpdateTask extends PleaseWaitRunnable {
-        private final Collection<PluginInformation> toUpdate;
-        public final Collection<PluginInformation> failed = new LinkedList<PluginInformation>();
-        private String errors = "";
-        private int count = 0;
-        private boolean restart;
-
-        private UpdateTask(Collection<PluginInformation> toUpdate, boolean up, boolean restart) {
-            super(up ? tr("Update Plugins") : tr("Download Plugins"));
-            this.toUpdate = toUpdate;
-            this.restart = restart;
-        }
-
-        @Override protected void cancel() {
-            finish();
-        }
-
-        @Override protected void finish() {
-            if (errors.length() > 0) {
-                JOptionPane.showMessageDialog(
-                        Main.parent,
-                        tr("There were problems with the following plugins:\n\n {0}",errors),
-                        tr("Error"),
-                        JOptionPane.ERROR_MESSAGE
-                );
-            } else {
-                String txt = trn("{0} Plugin successfully downloaded.", "{0} Plugins successfully downloaded.", count, count);
-                if(restart)
-                    txt += "\n"+tr("Please restart JOSM.");
-
-                JOptionPane.showMessageDialog(
-                        Main.parent,
-                        txt,
-                        tr("Information"),
-                        JOptionPane.INFORMATION_MESSAGE
-                );
-            }
-        }
-
-        @Override protected void realRun() throws SAXException, IOException {
-            File pluginDir = Main.pref.getPluginsDirFile();
-            if (!pluginDir.exists()) {
-                pluginDir.mkdirs();
-            }
-            progressMonitor.setTicksCount(toUpdate.size());
-            for (PluginInformation d : toUpdate) {
-                progressMonitor.subTask(tr("Downloading Plugin {0}...", d.name));
-                progressMonitor.worked(1);
-                File pluginFile = new File(pluginDir, d.name + ".jar.new");
-                if(download(d, pluginFile)) {
-                    count++;
-                } else
-                {
-                    errors += d.name + "\n";
-                    failed.add(d);
-                }
-            }
-            PluginDownloader.moveUpdatedPlugins();
-        }
-    }
-
-    private final static String[] pluginSites = {"http://josm.openstreetmap.de/plugin%<?plugins=>"};
-
-    public static Collection<String> getSites() {
-        return Main.pref.getCollection("pluginmanager.sites", Arrays.asList(pluginSites));
-    }
-    public static void setSites(Collection<String> c) {
-        Main.pref.putCollection("pluginmanager.sites", c);
-    }
-
-    public static int downloadDescription() {
-        int count = 0;
-        LinkedList<String> sitenames = new LinkedList<String>();
-        for (String site : getSites()) {
-            try {
-                String filesite = site.replaceAll("%<(.*)>", "");
-                /* replace %<x> with empty string or x=plugins (separated with comma) */
-                String pl = Main.pref.getCollectionAsString("plugins");
-                if(pl != null && pl.length() != 0)
-                    site = site.replaceAll("%<(.*)>", "$1"+pl);
-                else
-                    site = filesite;
-                BufferedReader r = new BufferedReader(new InputStreamReader(new URL(site).openStream(), "utf-8"));
-                new File(Main.pref.getPreferencesDir()+"plugins").mkdir();
-                String sname = count + "-site-" + filesite.replaceAll("[/:\\\\ <>|]", "_") + ".txt";
-                sitenames.add(sname);
-                BufferedWriter out = new BufferedWriter(new OutputStreamWriter(
-                        new FileOutputStream(new File(Main.pref.getPluginsDirFile(), sname)), "utf-8"));
-                for (String line = r.readLine(); line != null; line = r.readLine()) {
-                    out.append(line+"\n");
-                }
-                r.close();
-                out.close();
-                count++;
-            } catch (IOException x) {
-            }
-        }
-        /* remove old files */
-        File[] pluginFiles = Main.pref.getPluginsDirFile().listFiles();
-        if (pluginFiles != null) {
-            for (File f : pluginFiles) {
-                if (!f.isFile())
-                    continue;
-                String fname = f.getName();
-                if(fname.endsWith(".jar"))
-                {
-                    for(String s : PluginHandler.oldplugins)
-                    {
-                        if(fname.equals(s+".jar"))
-                        {
-                            System.out.println(tr("Delete old plugin {0}",fname));
-                            f.delete();
-                        }
-                    }
-                }
-                else if(!fname.endsWith(".jar.new") && !sitenames.contains(fname))
-                {
-                    System.out.println(tr("Delete old plugin file {0}",fname));
-                    f.delete();
-                }
-            }
-        }
-        return count;
-    }
-
-    private static boolean download(PluginInformation pd, File file) {
-        if(pd.mainversion > Version.getInstance().getVersion())
-        {
-            ExtendedDialog dialog = new ExtendedDialog(
-                    Main.parent,
-                    tr("Skip download"),
-                    new String[] {tr("Download Plugin"), tr("Skip Download")}
-            );
-            dialog.setContent(tr("JOSM version {0} required for plugin {1}.", pd.mainversion, pd.name));
-            dialog.setButtonIcons(new String[] {"download.png", "cancel.png"});
-            dialog.showDialog();
-            int answer = dialog.getValue();
-            if (answer != 1)
-                return false;
-        }
-
-        try {
-            InputStream in = new URL(pd.downloadlink).openStream();
-            OutputStream out = new FileOutputStream(file);
-            byte[] buffer = new byte[8192];
-            for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
-                out.write(buffer, 0, read);
-            }
-            out.close();
-            in.close();
-            new PluginInformation(file);
-            return true;
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-        file.delete(); /* cleanup */
-        return false;
-    }
-
-    public static void update(Collection<PluginInformation> update, boolean restart) {
-        Main.worker.execute(new UpdateTask(update, true, restart));
-    }
-
-    public Collection<PluginInformation> download(Collection<PluginInformation> download) {
-        // Execute task in current thread instead of executing it in other thread and waiting for result
-        // Waiting for result is not a good idea because the waiting thread will probably be either EDT
-        // or worker thread. Blocking one of these threads will cause deadlock
-        UpdateTask t = new UpdateTask(download, false, true);
-        t.run();
-        return t.failed;
-    }
-
-    public static boolean moveUpdatedPlugins() {
-        File pluginDir = Main.pref.getPluginsDirFile();
-        boolean ok = true;
-        if (pluginDir.exists() && pluginDir.isDirectory() && pluginDir.canWrite()) {
-            final File[] files = pluginDir.listFiles(new FilenameFilter() {
-                public boolean accept(File dir, String name) {
-                    return name.endsWith(".new");
-                }});
-            for (File updatedPlugin : files) {
-                final String filePath = updatedPlugin.getPath();
-                File plugin = new File(filePath.substring(0, filePath.length() - 4));
-                ok = (plugin.delete() || !plugin.exists()) && updatedPlugin.renameTo(plugin) && ok;
-            }
-        }
-        return ok;
-    }
-}
Index: trunk/src/org/openstreetmap/josm/plugins/PluginException.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginException.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginException.java	(revision 2817)
@@ -11,5 +11,5 @@
  * @author Immanuel.Scholz
  */
-public class PluginException extends RuntimeException {
+public class PluginException extends Exception {
     public final PluginProxy plugin;
     public final String name;
@@ -20,3 +20,15 @@
         this.name = name;
     }
+
+    public PluginException(String name, String message) {
+        super(message);
+        this.plugin = null;
+        this.name = name;
+    }
+
+    public PluginException(String name, Throwable cause) {
+        super(tr("An error occurred in plugin {0}", name), cause);
+        this.plugin = null;
+        this.name = name;
+    }
 }
Index: trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 2817)
@@ -4,8 +4,11 @@
 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
 import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
 
 import java.awt.Font;
 import java.awt.GridBagLayout;
 import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.FilenameFilter;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -14,9 +17,17 @@
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.SortedMap;
-import java.util.TreeMap;
+import java.util.Map;
+import java.util.Set;
 import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 import javax.swing.AbstractAction;
@@ -37,15 +48,24 @@
 import org.openstreetmap.josm.gui.download.DownloadSelection;
 import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
+import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
 
+/**
+ * PluginHandler is basically a collection of static utility functions used to bootstrap
+ * and manage the loaded plugins.
+ * 
+ */
 public class PluginHandler {
 
-    public static String [] oldplugins = new String[] {"mappaint", "unglueplugin",
-                "lang-de", "lang-en_GB", "lang-fr", "lang-it", "lang-pl", "lang-ro",
-                "lang-ru", "ewmsplugin", "ywms", "tways-0.2", "geotagged", "landsat",
-                "namefinder", "waypoints", "slippy_map_chooser", "tcx-support", "usertools",
-                "AgPifoJ", "utilsplugin"};
-    public static String [] unmaintained = new String[] {"gpsbabelgui", "Intersect_way"};
+    final public static String [] DEPRECATED_PLUGINS = new String[] {"mappaint", "unglueplugin",
+        "lang-de", "lang-en_GB", "lang-fr", "lang-it", "lang-pl", "lang-ro",
+        "lang-ru", "ewmsplugin", "ywms", "tways-0.2", "geotagged", "landsat",
+        "namefinder", "waypoints", "slippy_map_chooser", "tcx-support", "usertools",
+        "AgPifoJ", "utilsplugin"};
+
+    final public static String [] UNMAINTAINED_PLUGINS = new String[] {"gpsbabelgui", "Intersect_way"};
 
     /**
@@ -53,187 +73,522 @@
      */
     public final static Collection<PluginProxy> pluginList = new LinkedList<PluginProxy>();
-    /**
-     * Load all plugins specified in preferences. If the parameter is
-     * <code>true</code>, all early plugins are loaded (before constructor).
-     */
-    public static void loadPlugins(boolean early) {
-        List<String> plugins = new LinkedList<String>();
-        Collection<String> cp = Main.pref.getCollection("plugins", null);
-        if (cp != null) {
-            plugins.addAll(cp);
-        }
-        if (System.getProperty("josm.plugins") != null) {
-            plugins.addAll(Arrays.asList(System.getProperty("josm.plugins").split(",")));
-        }
-
-        for (String p : oldplugins) {
+
+
+    /**
+     * Removes deprecated plugins from a collection of plugins. Modifies the
+     * collection <code>plugins</code>.
+     * 
+     * Also notifies the user about removed deprecated plugins
+     * 
+     * @param plugins the collection of plugins
+     */
+    private static void filterDeprecatedPlugins(Collection<String> plugins) {
+        Set<String> removedPlugins = new HashSet<String>();
+        for (String p : DEPRECATED_PLUGINS) {
             if (plugins.contains(p)) {
                 plugins.remove(p);
                 Main.pref.removeFromCollection("plugins", p);
-                JOptionPane.showMessageDialog(
-                        Main.parent,
-                        tr("Plugin {0} is no longer necessary and has been deactivated.", p),
-                        tr("Warning"),
-                        JOptionPane.WARNING_MESSAGE
-                );
-            }
-        }
-        if(early)
-        {
-            for (String p : unmaintained) {
-                if (plugins.contains(p) && disablePlugin(tr("<html>Loading of {0} plugin was requested."
-                        +"<br>This plugin is no longer developed and very likely will produce errors."
-                        +"<br>It should be disabled.<br>Delete from preferences?</html>", p), p)) {
-                    plugins.remove(p);
-                }
-            }
-        }
-
-        if (plugins.isEmpty())
+                removedPlugins.add(p);
+            }
+        }
+        if (removedPlugins.isEmpty())
             return;
 
-        if(early)
-        {
-            String doUpdate = null;
-            String check = null;
-            int v = Version.getInstance().getVersion();
-            if(Main.pref.getInteger("pluginmanager.version", 0) < v)
-            {
-                doUpdate = tr("You updated your JOSM software.\nTo prevent problems the plugins should be updated as well.\n"
-                        + "Update plugins now?");
-                check = "pluginmanger.version";
-            }
-            else
-            {
-                long tim = System.currentTimeMillis();
-                long last = Main.pref.getLong("pluginmanager.lastupdate", 0);
-                Integer maxTime = Main.pref.getInteger("pluginmanager.warntime", 60);
-                long d = (tim - last)/(24*60*60*1000l);
-                if ((last <= 0) || (maxTime <= 0)) {
-                    Main.pref.put("pluginmanager.lastupdate",Long.toString(tim));
-                } else if (d > maxTime) {
-                    doUpdate = tr("Last plugin update more than {0} days ago.", d);
-                    check = "pluginmanager.time";
-                }
-            }
-            if(doUpdate != null)
-            {
-                ExtendedDialog dialog = new ExtendedDialog(
-                        Main.parent,
-                        tr("Update plugins"),
-                        new String[] {tr("Update plugins"), tr("Skip update")}
-                );
-                dialog.setContent(doUpdate);
-                dialog.toggleEnable(check);
-                dialog.setButtonIcons( new String[] {"dialogs/refresh.png", "cancel.png"});
-                dialog.configureContextsensitiveHelp(ht("/Plugin/AutomaticUpdate"), true /* show help button */);
-                dialog.showDialog();
-                if(dialog.getValue() == 1) {
-                    new PluginSelection().update();
-                }
-            }
-        }
-
-        SortedMap<Integer, Collection<PluginInformation>> p = new TreeMap<Integer, Collection<PluginInformation>>();
-        for (String pluginName : plugins) {
-            PluginInformation info = PluginInformation.findPlugin(pluginName);
-            if (info != null) {
-                if (info.early != early) {
-                    continue;
-                }
-                int josmVersion = Version.getInstance().getVersion();
-                if (info.mainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) {
-                    JOptionPane.showMessageDialog(
-                            Main.parent,
-                            tr("Plugin {0} requires JOSM update to version {1}.", pluginName,
-                                    info.mainversion),
-                                    tr("Warning"),
-                                    JOptionPane.WARNING_MESSAGE
-                    );
-                    continue;
-                }
-
-                if(info.requires != null)
-                {
-                    String warn = null;
-                    for(String n : info.requires.split(";"))
-                    {
-                        if(!plugins.contains(n))
-                        { warn = n; break; }
-                    }
-                    if(warn != null)
-                    {
-                        JOptionPane.showMessageDialog(Main.parent,
-                                tr("Plugin {0} is required by plugin {1} but was not found.",
-                                        warn, pluginName),
-                                        tr("Error"),
-                                        JOptionPane.ERROR_MESSAGE
-                        );
-                        continue;
-                    }
-                }
-                if (!p.containsKey(info.stage)) {
-                    p.put(info.stage, new LinkedList<PluginInformation>());
-                }
-                p.get(info.stage).add(info);
-            } else if(early) {
-                JOptionPane.showMessageDialog(
-                        Main.parent,
-                        tr("Plugin not found: {0}.", pluginName),
-                        tr("Error"),
-                        JOptionPane.ERROR_MESSAGE
-                );
-            }
-        }
-
+        // notify user about removed deprecated plugins
+        //
+        StringBuffer sb = new StringBuffer();
+        sb.append("<html>");
+        sb.append(trn(
+                "The following plugin is no longer necessary and has been deactivated:",
+                "The following plugins are no longer necessary and have been deactivated:",
+                removedPlugins.size()
+        ));
+        sb.append("<ul>");
+        for (String name: removedPlugins) {
+            sb.append("<li>").append(name).append("</li>");
+        }
+        sb.append("</ul>");
+        sb.append("</html>");
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                sb.toString(),
+                tr("Warning"),
+                JOptionPane.WARNING_MESSAGE
+        );
+    }
+
+    /**
+     * Removes unmaintained plugins from a collection of plugins. Modifies the
+     * collection <code>plugins</code>. Also removes the plugin from the list
+     * of plugins in the preferences, if necessary.
+     * 
+     * Asks the user for every unmaintained plugin whether it should be removed.
+     * 
+     * @param plugins the collection of plugins
+     */
+    private static void filterUnmaintainedPlugins(Collection<String> plugins) {
+        for (String unmaintained : UNMAINTAINED_PLUGINS) {
+            if (!plugins.contains(unmaintained)) {
+                continue;
+            }
+            String msg =  tr("<html>Loading of {0} plugin was requested."
+                    + "<br>This plugin is no longer developed and very likely will produce errors."
+                    +"<br>It should be disabled.<br>Delete from preferences?</html>", unmaintained);
+            if (confirmDisablePlugin(msg,unmaintained)) {
+                Main.pref.removeFromCollection("plugins", unmaintained);
+                plugins.remove(unmaintained);
+            }
+        }
+    }
+
+    /**
+     * Checks whether the locally available plugins should be updated and
+     * asks the user if running an update is OK. An update is advised if
+     * JOSM was updated to a new version since the last plugin updates or
+     * if the plugins were last updated a long time ago.
+     * 
+     * @return true if a plugin update should be run; false, otherwise
+     */
+    public static boolean checkAndConfirmPluginUpdate() {
+        String message = null;
+        String togglePreferenceKey = null;
+        int v = Version.getInstance().getVersion();
+        if (Main.pref.getInteger("pluginmanager.version", 0) < v) {
+            message = tr("<html>You updated your JOSM software.<br>"
+                    + "To prevent problems the plugins should be updated as well.<br><br>"
+                    + "Update plugins now?"
+                    + "</html>"
+            );
+            togglePreferenceKey = "pluginmanger.version";
+        }  else {
+            long tim = System.currentTimeMillis();
+            long last = Main.pref.getLong("pluginmanager.lastupdate", 0);
+            Integer maxTime = Main.pref.getInteger("pluginmanager.warntime", 60);
+            long d = (tim - last) / (24 * 60 * 60 * 1000l);
+            if ((last <= 0) || (maxTime <= 0)) {
+                Main.pref.put("pluginmanager.lastupdate", Long.toString(tim));
+            } else if (d > maxTime) {
+                message = tr("Last plugin update more than {0} days ago.", d);
+                togglePreferenceKey = "pluginmanager.time";
+            }
+        }
+        if (message == null) return false;
+
+        // ask whether update is fine
+        //
+        ExtendedDialog dialog = new ExtendedDialog(
+                Main.parent,
+                tr("Update plugins"),
+                new String[] {
+                    tr("Update plugins"), tr("Skip update")
+                }
+        );
+        dialog.setContent(message);
+        dialog.toggleEnable(togglePreferenceKey);
+        dialog.setButtonIcons( new String[] {"dialogs/refresh.png", "cancel.png"});
+        dialog.configureContextsensitiveHelp(ht("/Plugin/AutomaticUpdate"), true /* show help button */);
+        dialog.showDialog();
+        return dialog.getValue() == 1;
+    }
+
+    /**
+     * Alerts the user if a plugin required by another plugin is missing
+     * 
+     * @param plugin the the plugin
+     * @param missingRequiredPlugin the missing required plugin
+     */
+    private static void alertMissingRequiredPlugin(String plugin, Set<String> missingRequiredPlugin) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("<html>");
+        sb.append(trn("A required plugin for plugin {0} was not found. The required plugin is:",
+                "{1} required plugins for plugin {0} were not found. The required plugins are:",
+                missingRequiredPlugin.size(),
+                plugin,
+                missingRequiredPlugin.size()
+        ));
+        sb.append("<ul>");
+        for (String p: missingRequiredPlugin) {
+            sb.append("<li>").append(p).append("</li>");
+        }
+        sb.append("</ul>").append("</html>");
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                sb.toString(),
+                tr("Error"),
+                JOptionPane.ERROR_MESSAGE
+        );
+    }
+
+    /**
+     * Checks whether all preconditions for loading the plugin <code>plugin</code> are met. The
+     * current JOSM version must be compatible with the plugin and no other plugins this plugin
+     * depends on should be missing.
+     * 
+     * @param plugins the collection of all loaded plugins
+     * @param plugin the plugin for which preconditions are checked
+     * @return true, if the preconditions are met; false otherwise
+     */
+    public static boolean checkLoadPreconditions(Collection<PluginInformation> plugins, PluginInformation plugin) {
+
+        // make sure the plugin is compatible with the current JOSM version
+        //
+        int josmVersion = Version.getInstance().getVersion();
+        if (plugin.mainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) {
+            JOptionPane.showMessageDialog(
+                    Main.parent,
+                    tr("Plugin {0} requires JOSM update to version {1}.", plugin.name,
+                            plugin.mainversion),
+                            tr("Warning"),
+                            JOptionPane.WARNING_MESSAGE
+            );
+            return false;
+        }
+
+        // make sure the dependencies to other plugins are not broken
+        //
+        if(plugin.requires != null){
+            Set<String> pluginNames = new HashSet<String>();
+            for (PluginInformation pi: plugins) {
+                pluginNames.add(pi.name);
+            }
+            Set<String> missingPlugins = new HashSet<String>();
+            for (String requiredPlugin : plugin.requires.split(";")) {
+                if (!pluginNames.contains(requiredPlugin)) {
+                    missingPlugins.add(requiredPlugin);
+                }
+            }
+            if (!missingPlugins.isEmpty()) {
+                alertMissingRequiredPlugin(plugin.name, missingPlugins);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Creates a class loader for loading plugin code.
+     * 
+     * @param plugins the collection of plugins which are going to be loaded with this
+     * class loader
+     * @return the class loader
+     */
+    public static ClassLoader createClassLoader(Collection<PluginInformation> plugins) {
         // iterate all plugins and collect all libraries of all plugins:
-        List<URL> allPluginLibraries = new ArrayList<URL>();
-        for (Collection<PluginInformation> c : p.values()) {
-            for (PluginInformation info : c) {
-                allPluginLibraries.addAll(info.libraries);
-            }
-        }
+        List<URL> allPluginLibraries = new LinkedList<URL>();
+        File pluginDir = Main.pref.getPluginsDirectory();
+        for (PluginInformation info : plugins) {
+            if (info.libraries == null) {
+                continue;
+            }
+            allPluginLibraries.addAll(info.libraries);
+            File pluginJar = new File(pluginDir, info.name + ".jar");
+            URL pluginJarUrl = PluginInformation.fileToURL(pluginJar);
+            allPluginLibraries.add(pluginJarUrl);
+        }
+
         // create a classloader for all plugins:
         URL[] jarUrls = new URL[allPluginLibraries.size()];
         jarUrls = allPluginLibraries.toArray(jarUrls);
         URLClassLoader pluginClassLoader = new URLClassLoader(jarUrls, Main.class.getClassLoader());
-        ImageProvider.sources.add(0, pluginClassLoader);
-
-        for (Collection<PluginInformation> c : p.values()) {
-            for (PluginInformation info : c) {
-                try {
-                    Class<?> klass = info.loadClass(pluginClassLoader);
-                    if (klass != null) {
-                        System.out.println("loading "+info.name);
-                        pluginList.add(info.load(klass));
+        return pluginClassLoader;
+    }
+
+    /**
+     * Loads and instantiates the plugin described by <code>plugin</code> using
+     * the class loader <code>pluginClassLoader</code>.
+     * 
+     * @param plugin the plugin
+     * @param pluginClassLoader the plugin class loader
+     */
+    public static void loadPlugin(PluginInformation plugin, ClassLoader pluginClassLoader) {
+        try {
+            Class<?> klass = plugin.loadClass(pluginClassLoader);
+            if (klass != null) {
+                System.out.println(tr("loading plugin ''{0}''", plugin.name));
+                pluginList.add(plugin.load(klass));
+            }
+        } catch (Throwable e) {
+            e.printStackTrace();
+            String msg = tr("Could not load plugin {0}. Delete from preferences?", plugin.name);
+            if (confirmDisablePlugin(msg, plugin.name)) {
+                Main.pref.removeFromCollection("plugins", plugin.name);
+            }
+        }
+    }
+
+    /**
+     * Loads the plugin in <code>plugins</code> from locally available jar files into
+     * memory.
+     * 
+     * @param plugins the list of plugins
+     * @param monitor the progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null.
+     */
+    public static void loadPlugins(Collection<PluginInformation> plugins, ProgressMonitor monitor) {
+        if (monitor == null) {
+            monitor = NullProgressMonitor.INSTANCE;
+        }
+        try {
+            monitor.beginTask(tr("Loading plugins ..."));
+            List<PluginInformation> toLoad = new LinkedList<PluginInformation>();
+            // sort the plugins according to their "staging" equivalence class. The
+            // lower the value of "stage" the earlier the plugin should be loaded.
+            //
+            Collections.sort(
+                    toLoad,
+                    new Comparator<PluginInformation>() {
+                        public int compare(PluginInformation o1, PluginInformation o2) {
+                            if (o1.stage < o2.stage) return -1;
+                            if (o1.stage == o2.stage) return 0;
+                            return 1;
+                        }
                     }
-                } catch (Throwable e) {
-                    e.printStackTrace();
-                    disablePlugin(tr("Could not load plugin {0}. Delete from preferences?", info.name), info.name);
-                }
-            }
-        }
-    }
-    public static boolean disablePlugin(String reason, String name)
-    {
+            );
+            monitor.subTask(tr("Checking plugin preconditions..."));
+            for (PluginInformation pi: plugins) {
+                if (checkLoadPreconditions(plugins, pi)) {
+                    toLoad.add(pi);
+                }
+            }
+            if (toLoad.isEmpty())
+                return;
+
+            ClassLoader pluginClassLoader = createClassLoader(toLoad);
+            ImageProvider.sources.add(0, pluginClassLoader);
+            monitor.setTicksCount(toLoad.size());
+            for (PluginInformation info : toLoad) {
+                monitor.setExtraText(tr("Loading plugin ''{0}''...", info.name));
+                loadPlugin(info, pluginClassLoader);
+                monitor.worked(1);
+            }
+        } finally {
+            monitor.finishTask();
+        }
+    }
+
+    /**
+     * Loads plugins from <code>plugins</code> which have the flag {@see PluginInformation#early}
+     * set to true.
+     * 
+     * @param plugins the collection of plugins
+     * @param monitor the progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null.
+     */
+    public static void loadEarlyPlugins(Collection<PluginInformation> plugins, ProgressMonitor monitor) {
+        List<PluginInformation> earlyPlugins = new ArrayList<PluginInformation>(plugins.size());
+        for (PluginInformation pi: plugins) {
+            if (pi.early) {
+                earlyPlugins.add(pi);
+            }
+        }
+        loadPlugins(earlyPlugins, monitor);
+    }
+
+    /**
+     * Loads plugins from <code>plugins</code> which have the flag {@see PluginInformation#early}
+     * set to false.
+     * 
+     * @param plugins the collection of plugins
+     * @param monitor the progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null.
+     */
+    public static void loadLatePlugins(Collection<PluginInformation> plugins, ProgressMonitor monitor) {
+        List<PluginInformation> latePlugins = new ArrayList<PluginInformation>(plugins.size());
+        for (PluginInformation pi: plugins) {
+            if (!pi.early) {
+                latePlugins.add(pi);
+            }
+        }
+        loadPlugins(latePlugins, monitor);
+    }
+
+    /**
+     * Loads locally available plugin information from local plugin jars and from cached
+     * plugin lists.
+     *
+     * @param monitor the progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null.
+     * @return the list of locally available plugin information
+     * 
+     */
+    private static Map<String, PluginInformation> loadLocallyAvailablePluginInformation(ProgressMonitor monitor) {
+        if (monitor == null) {
+            monitor = NullProgressMonitor.INSTANCE;
+        }
+        try {
+            ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(monitor);
+            ExecutorService service = Executors.newSingleThreadExecutor();
+            Future<?> future = service.submit(task);
+            try {
+                future.get();
+            } catch(ExecutionException e) {
+                e.printStackTrace();
+                return null;
+            } catch(InterruptedException e) {
+                e.printStackTrace();
+                return null;
+            }
+            HashMap<String, PluginInformation> ret = new HashMap<String, PluginInformation>();
+            for (PluginInformation pi: task.getAvailablePlugins()) {
+                ret.put(pi.name, pi);
+            }
+            return ret;
+        } finally {
+            monitor.finishTask();
+        }
+    }
+
+    private static void alertMissingPluginInformation(Collection<String> plugins) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("<html>");
+        sb.append(trn("JOSM could not find information about the following plugin:",
+                "JOSM could not find information about the following plugins:",
+                plugins.size()));
+        sb.append("<ul>");
+        for (String plugin: plugins) {
+            sb.append("<li>").append(plugin).append("</li>");
+        }
+        sb.append("</ul>");
+        sb.append(trn("The plugin is not going to be loaded.",
+                "The plugins are not going to be loaded.",
+                plugins.size()));
+        sb.append("</html>");
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                sb.toString(),
+                tr("Warning"),
+                JOptionPane.WARNING_MESSAGE
+        );
+    }
+
+    /**
+     * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered
+     * out. This involves user interaction. This method displays alert and confirmation
+     * messages.
+     *
+     * @param monitor the progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null.
+     * @return the set of plugins to load (as set of plugin names)
+     */
+    public static List<PluginInformation> buildListOfPluginsToLoad(ProgressMonitor monitor) {
+        if (monitor == null) {
+            monitor = NullProgressMonitor.INSTANCE;
+        }
+        try {
+            monitor.beginTask(tr("Determine plugins to load..."));
+            Set<String> plugins = new HashSet<String>();
+            plugins.addAll(Main.pref.getCollection("plugins",  new LinkedList<String>()));
+            if (System.getProperty("josm.plugins") != null) {
+                plugins.addAll(Arrays.asList(System.getProperty("josm.plugins").split(",")));
+            }
+            monitor.subTask(tr("Removing deprecated plugins..."));
+            filterDeprecatedPlugins(plugins);
+            monitor.subTask(tr("Removing umaintained plugins..."));
+            filterUnmaintainedPlugins(plugins);
+            Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1,false));
+            List<PluginInformation> ret = new LinkedList<PluginInformation>();
+            for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
+                String plugin = it.next();
+                if (infos.containsKey(plugin)) {
+                    ret.add(infos.get(plugin));
+                    it.remove();
+                }
+            }
+            if (!plugins.isEmpty()) {
+                alertMissingPluginInformation(plugins);
+            }
+            return ret;
+        } finally {
+            monitor.finishTask();
+        }
+    }
+
+    private static void alertFailedPluginUpdate(Collection<PluginInformation> plugins) {
+        StringBuffer sb = new StringBuffer();
+        sb.append("<html>");
+        sb.append(trn(
+                "Updating the following plugin has failed:",
+                "Updating the following plugins has failed:",
+                plugins.size()
+        )
+        );
+        sb.append("<ul>");
+        for (PluginInformation pi: plugins) {
+            sb.append("<li>").append(pi.name).append("</li>");
+        }
+        sb.append("</ul>");
+        sb.append(tr("Please open the Preference Dialog after JOSM has started and try to update them manually."));
+        sb.append("</html>");
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                sb.toString(),
+                tr("Plugin update failed"),
+                JOptionPane.ERROR_MESSAGE
+        );
+    }
+
+    /**
+     * Updates the plugins in <code>plugins</code>.
+     * 
+     * @param plugins the collection of plugins to update. Must not be null.
+     * @param monitor the progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null.
+     * @throws IllegalArgumentException thrown if plugins is null
+     */
+    public static void updatePlugins(Collection<PluginInformation> plugins, ProgressMonitor monitor) throws IllegalArgumentException{
+        CheckParameterUtil.ensureParameterNotNull(plugins, "plugins");
+        if (monitor == null) {
+            monitor = NullProgressMonitor.INSTANCE;
+        }
+        try {
+            PluginDownloadTask task = new PluginDownloadTask(
+                    monitor,
+                    plugins,
+                    tr("Update plugins")
+            );
+            ExecutorService service = Executors.newSingleThreadExecutor();
+            Future<?> future = service.submit(task);
+            try {
+                future.get();
+            } catch(ExecutionException e) {
+                e.printStackTrace();
+            } catch(InterruptedException e) {
+                e.printStackTrace();
+            }
+            if (! task.getFailedPlugins().isEmpty()) {
+                alertFailedPluginUpdate(task.getFailedPlugins());
+                return;
+            }
+        } finally {
+            monitor.finishTask();
+        }
+        // remember the update because it was successful
+        //
+        Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
+        Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
+    }
+
+    /**
+     * Ask the user for confirmation that a plugin shall be disabled.
+     * 
+     * @param reason the reason for disabling the plugin
+     * @param name the plugin name
+     * @return true, if the plugin shall be disabled; false, otherwise
+     */
+    public static boolean confirmDisablePlugin(String reason, String name) {
         ExtendedDialog dialog = new ExtendedDialog(
                 Main.parent,
                 tr("Disable plugin"),
-                new String[] {tr("Disable plugin"), tr("Keep plugin")}
+                new String[] {
+                    tr("Disable plugin"), tr("Keep plugin")
+                }
         );
         dialog.setContent(reason);
-        dialog.setButtonIcons( new String[] {"dialogs/delete.png", "cancel.png"});
+        dialog.setButtonIcons(new String[] { "dialogs/delete.png", "cancel.png" });
         dialog.showDialog();
-        int result = dialog.getValue();
-
-        if(result == 1)
-        {
-            Main.pref.removeFromCollection("plugins", name);
-            return true;
-        }
-        return false;
-    }
-
-    public static void setMapFrame(MapFrame old, MapFrame map) {
+        return dialog.getValue() == 1;
+    }
+
+    /**
+     * Notified loaded plugins about a new map frame
+     * 
+     * @param old the old map frame
+     * @param map the new map frame
+     */
+    public static void notifyMapFrameChanged(MapFrame old, MapFrame map) {
         for (PluginProxy plugin : pluginList) {
             plugin.mapFrameInitialized(old, map);
@@ -248,12 +603,11 @@
     }
 
-    public static void addDownloadSelection(List<DownloadSelection> downloadSelections)
-    {
+    public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
         for (PluginProxy p : pluginList) {
             p.addDownloadSelection(downloadSelections);
         }
     }
-    public static void getPreferenceSetting(Collection<PreferenceSettingFactory> settings)
-    {
+
+    public static void getPreferenceSetting(Collection<PreferenceSettingFactory> settings) {
         for (PluginProxy plugin : pluginList) {
             settings.add(new PluginPreferenceFactory(plugin));
@@ -261,14 +615,39 @@
     }
 
-    public static void earlyCleanup()
-    {
-        if (!PluginDownloader.moveUpdatedPlugins()) {
-            JOptionPane.showMessageDialog(
-                    Main.parent,
-                    tr("Activating the updated plugins failed. Check if JOSM has the permission to overwrite the existing ones."),
-                    tr("Plugins"), JOptionPane.ERROR_MESSAGE);
-        }
-    }
-    public static Boolean checkException(Throwable e)
+    /**
+     * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
+     * ".jar" files.
+     * 
+     */
+    public static void installDownloadedPlugins() {
+        File pluginDir = Main.pref.getPluginsDirectory();
+        if (! pluginDir.exists() || ! pluginDir.isDirectory() || ! pluginDir.canWrite())
+            return;
+
+        final File[] files = pluginDir.listFiles(new FilenameFilter() {
+            public boolean accept(File dir, String name) {
+                return name.endsWith(".jar.new");
+            }});
+
+        for (File updatedPlugin : files) {
+            final String filePath = updatedPlugin.getPath();
+            File plugin = new File(filePath.substring(0, filePath.length() - 4));
+            String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
+            if (plugin.exists()) {
+                if (!plugin.delete()) {
+                    System.err.println(tr("Warning: failed to delete outdated plugin ''{0}''.", plugin.toString()));
+                    System.err.println(tr("Warning: failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM still going to load the old plugin version.", pluginName));
+                    continue;
+                }
+            }
+            if (!updatedPlugin.renameTo(plugin)) {
+                System.err.println(tr("Warning: failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.", plugin.toString(), updatedPlugin.toString()));
+                System.err.println(tr("Warning: failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM still going to load the old plugin version.", pluginName));
+            }
+        }
+        return;
+    }
+
+    public static boolean checkException(Throwable e)
     {
         PluginProxy plugin = null;
@@ -350,27 +729,31 @@
         return false;
     }
-    public static String getBugReportText()
-    {
+
+    public static String getBugReportText() {
         String text = "";
         String pl = Main.pref.getCollectionAsString("plugins");
-        if(pl != null && pl.length() != 0) {
-            text += "Plugins: "+pl+"\n";
+        if (pl != null && pl.length() != 0) {
+            text += "Plugins: " + pl + "\n";
         }
         for (final PluginProxy pp : pluginList) {
-            text += "Plugin " + pp.info.name + (pp.info.version != null && !pp.info.version.equals("") ? " Version: "+pp.info.version+"\n" : "\n");
+            text += "Plugin "
+                + pp.info.name
+                + (pp.info.version != null && !pp.info.version.equals("") ? " Version: " + pp.info.version + "\n"
+                        : "\n");
         }
         return text;
     }
-    public static JPanel getInfoPanel()
-    {
+
+    public static JPanel getInfoPanel() {
         JPanel pluginTab = new JPanel(new GridBagLayout());
         for (final PluginProxy p : pluginList) {
-            String name = p.info.name + (p.info.version != null && !p.info.version.equals("") ? " Version: "+p.info.version : "");
+            String name = p.info.name
+            + (p.info.version != null && !p.info.version.equals("") ? " Version: " + p.info.version : "");
             pluginTab.add(new JLabel(name), GBC.std());
             pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
-            pluginTab.add(new JButton(new AbstractAction(tr("Information")){
+            pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
                 public void actionPerformed(ActionEvent event) {
                     StringBuilder b = new StringBuilder();
-                    for (Entry<String,String> e : p.info.attr.entrySet()) {
+                    for (Entry<String, String> e : p.info.attr.entrySet()) {
                         b.append(e.getKey());
                         b.append(": ");
@@ -378,22 +761,19 @@
                         b.append("\n");
                     }
-                    JTextArea a = new JTextArea(10,40);
+                    JTextArea a = new JTextArea(10, 40);
                     a.setEditable(false);
                     a.setText(b.toString());
-                    JOptionPane.showMessageDialog(
-                            Main.parent,
-                            new JScrollPane(a),
-                            tr("Plugin information"),
-                            JOptionPane.INFORMATION_MESSAGE
-                    );
+                    JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
+                            JOptionPane.INFORMATION_MESSAGE);
                 }
             }), GBC.eol());
 
-            JTextArea description = new JTextArea((p.info.description==null? tr("no description available"):p.info.description));
+            JTextArea description = new JTextArea((p.info.description == null ? tr("no description available")
+                    : p.info.description));
             description.setEditable(false);
             description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
             description.setLineWrap(true);
             description.setWrapStyleWord(true);
-            description.setBorder(BorderFactory.createEmptyBorder(0,20,0,0));
+            description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
             description.setBackground(UIManager.getColor("Panel.background"));
 
Index: trunk/src/org/openstreetmap/josm/plugins/PluginInformation.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginInformation.java	(revision 2815)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginInformation.java	(revision 2817)
@@ -43,4 +43,5 @@
     public int stage = 50;
     public String version = null;
+    public String localversion = null;
     public String downloadlink = null;
     public List<URL> libraries = new LinkedList<URL>();
@@ -60,25 +61,31 @@
      * @param file the plugin jar file.
      */
-    public PluginInformation(File file) {
+    public PluginInformation(File file) throws PluginException{
         this(file, file.getName().substring(0, file.getName().length()-4));
     }
 
-    public PluginInformation(File file, String name) {
+    public PluginInformation(File file, String name) throws PluginException{
         this.name = name;
         this.file = file;
+        JarInputStream jar = null;
         try {
-            JarInputStream jar = new JarInputStream(new FileInputStream(file));
+            jar = new JarInputStream(new FileInputStream(file));
             Manifest manifest = jar.getManifest();
             if (manifest == null)
-                throw new IOException(file+" contains no manifest.");
+                throw new PluginException(name, tr("The plugin file ''{0}'' doesn't include a Manifest.", file.toString()));
             scanManifest(manifest, false);
             libraries.add(0, fileToURL(file));
-            jar.close();
         } catch (IOException e) {
-            throw new PluginException(null, name, e);
-        }
-    }
-
-    public PluginInformation(InputStream manifestStream, String name, String url) {
+            throw new PluginException(name, e);
+        } finally {
+            if (jar != null) {
+                try {
+                    jar.close();
+                } catch(IOException e) { /* ignore */ }
+            }
+        }
+    }
+
+    public PluginInformation(InputStream manifestStream, String name, String url) throws PluginException {
         this.name = name;
         try {
@@ -90,5 +97,5 @@
             scanManifest(manifest, url != null);
         } catch (IOException e) {
-            throw new PluginException(null, name, e);
+            throw new PluginException(name, e);
         }
     }
@@ -167,11 +174,19 @@
     }
 
-    public String getLinkDescription()
-    {
-        String d = description == null ? tr("no description available") : description;
-        if(link != null) {
-            d += " <A HREF=\""+link+"\">"+tr("More details")+"</A>";
-        }
-        return d;
+    /**
+     * Replies the description as HTML document, including a link to a web page with
+     * more information, provided such a link is available.
+     * 
+     * @return the description as HTML document
+     */
+    public String getDescriptionAsHtml() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("<html><body>");
+        sb.append(description == null ? tr("no description available") : description);
+        if (link != null) {
+            sb.append(" <a href=\"").append(link).append("\">").append(tr("More info...")).append("</a>");
+        }
+        sb.append("</body></html>");
+        return sb.toString();
     }
 
@@ -179,10 +194,12 @@
      * Load and instantiate the plugin
      */
-    public PluginProxy load(Class<?> klass) {
+    public PluginProxy load(Class<?> klass) throws PluginException{
         try {
             currentPluginInitialization = this;
             return new PluginProxy(klass.newInstance(), this);
-        } catch (Exception e) {
-            throw new PluginException(null, name, e);
+        } catch(IllegalAccessException e) {
+            throw new PluginException(name, e);
+        } catch (InstantiationException e) {
+            throw new PluginException(name, e);
         }
     }
@@ -191,12 +208,12 @@
      * Load the class of the plugin
      */
-    public Class<?> loadClass(ClassLoader classLoader) {
+    public Class<?> loadClass(ClassLoader classLoader) throws PluginException {
         if (className == null)
             return null;
-        try {
+        try{
             Class<?> realClass = Class.forName(className, true, classLoader);
             return realClass;
-        } catch (Exception e) {
-            throw new PluginException(null, name, e);
+        } catch (ClassNotFoundException e) {
+            throw new PluginException(name, e);
         }
     }
@@ -272,3 +289,74 @@
         return all;
     }
+
+    /**
+     * Replies true if the plugin with the given information is most likely outdated with
+     * respect to the referenceVersion.
+     * 
+     * @param referenceVersion the reference version. Can be null if we don't know a
+     * reference version
+     * 
+     * @return true, if the plugin needs to be updated; false, otherweise
+     */
+    public boolean isUpdateRequired(String referenceVersion) {
+        if (this.version == null && referenceVersion!= null)
+            return true;
+        if (this.version != null && !this.version.equals(referenceVersion))
+            return true;
+        return false;
+    }
+
+    /**
+     * Replies true if this this plugin should be updated/downloaded because either
+     * it is not available locally (its local version is null) or its local version is
+     * older than the available version on the server.
+     * 
+     * @return true if the plugin should be updated
+     */
+    public boolean isUpdateRequired() {
+        if (this.localversion == null) return false;
+        return isUpdateRequired(this.localversion);
+    }
+
+    protected boolean matches(String filter, String value) {
+        if (filter == null) return true;
+        if (value == null) return false;
+        return value.toLowerCase().contains(filter.toLowerCase());
+    }
+
+    /**
+     * Replies true if either the name, the description, or the version match (case insensitive)
+     * one of the words in filter. Replies true if filter is null.
+     * 
+     * @param filter the filter expression
+     * @return true if this plugin info matches with the filter
+     */
+    public boolean matches(String filter) {
+        if (filter == null) return true;
+        String words[] = filter.split("\\s+");
+        for (String word: words) {
+            if (matches(word, name)
+                    || matches(word, description)
+                    || matches(word, version)
+                    || matches(word, localversion))
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Replies the name of the plugin
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name
+     * @param name
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/plugins/PluginListParseException.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginListParseException.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginListParseException.java	(revision 2817)
@@ -0,0 +1,20 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins;
+
+public class PluginListParseException extends Exception {
+    public PluginListParseException() {
+        super();
+    }
+
+    public PluginListParseException(String arg0, Throwable arg1) {
+        super(arg0, arg1);
+    }
+
+    public PluginListParseException(String arg0) {
+        super(arg0);
+    }
+
+    public PluginListParseException(Throwable arg0) {
+        super(arg0);
+    }
+}
Index: trunk/src/org/openstreetmap/josm/plugins/PluginListParser.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginListParser.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/plugins/PluginListParser.java	(revision 2817)
@@ -0,0 +1,100 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A parser for the plugin list provided by a JOSM Plugin Download Site.
+ * 
+ * See <a href="http://josm.openstreetmap.de/plugin">http://josm.openstreetmap.de/plugin</a>
+ * for a sample of the document. The format is a custom format, kind of mix of CSV and RFC822 style
+ * name/value-pairs.
+ *
+ */
+public class PluginListParser {
+
+    /**
+     * Creates the plugin information object
+     * 
+     * @param name the plugin name
+     * @param url the plugin download url
+     * @param manifest the plugin manifest
+     * @return a plugin information object
+     * @throws PluginListParseException
+     */
+    protected PluginInformation createInfo(String name, String url, String manifest) throws PluginListParseException{
+        try {
+            return new PluginInformation(
+                    new ByteArrayInputStream(manifest.getBytes("utf-8")),
+                    name.substring(0, name.length() - 4),
+                    url
+            );
+        } catch(UnsupportedEncodingException e) {
+            throw new PluginListParseException(tr("Failed to create plugin information from manifest for plugin ''{0}''", name), e);
+        } catch (PluginException e) {
+            throw new PluginListParseException(tr("Failed to create plugin information from manifest for plugin ''{0}''", name), e);
+        }
+    }
+
+    /**
+     * Parses a plugin information document and replies a list of plugin information objects.
+     * 
+     * See <a href="http://josm.openstreetmap.de/plugin">http://josm.openstreetmap.de/plugin</a>
+     * for a sample of the document. The format is a custom format, kind of mix of CSV and RFC822 style
+     * name/value-pairs.
+     * 
+     * @param in the input stream from which to parse
+     * @return the list of plugin information objects
+     * @throws PluginListParseException thrown if something goes wrong while parsing
+     */
+    public List<PluginInformation> parse(InputStream in) throws PluginListParseException{
+        List<PluginInformation> ret = new LinkedList<PluginInformation>();
+        BufferedReader r = null;
+        try {
+            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
+            String name = null;
+            String url = null;
+            StringBuilder manifest = new StringBuilder();
+            for (String line = r.readLine(); line != null; line = r.readLine()) {
+                if (line.startsWith("\t")) {
+                    line = line.substring(1);
+                    if (line.length() > 70) {
+                        manifest.append(line.substring(0, 70)).append("\n");
+                        line = " " + line.substring(70);
+                    }
+                    manifest.append(line).append("\n");
+                    continue;
+                }
+                if (name != null) {
+                    PluginInformation info = createInfo(name, url, manifest.toString());
+                    if (info != null) {
+                        ret.add(info);
+                    }
+                }
+                String x[] = line.split(";");
+                name = x[0];
+                url = x[1];
+                manifest = new StringBuilder();
+
+            }
+            if (name != null) {
+                PluginInformation info = createInfo(name, url, manifest.toString());
+                if (info != null) {
+                    ret.add(info);
+                }
+            }
+            return ret;
+        } catch (IOException e) {
+            throw new PluginListParseException(e);
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/plugins/PluginSelection.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/PluginSelection.java	(revision 2815)
+++ 	(revision )
@@ -1,438 +1,0 @@
-//License: GPL. Copyright 2007 by Immanuel Scholz and others
-package org.openstreetmap.josm.plugins;
-
-import static org.openstreetmap.josm.tools.I18n.tr;
-import static org.openstreetmap.josm.tools.I18n.trn;
-
-import java.awt.GridBagConstraints;
-import java.awt.Insets;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStreamReader;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.Map.Entry;
-
-import javax.swing.BorderFactory;
-import javax.swing.JCheckBox;
-import javax.swing.JEditorPane;
-import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JTextField;
-import javax.swing.UIManager;
-import javax.swing.event.HyperlinkEvent;
-import javax.swing.event.HyperlinkListener;
-import javax.swing.event.HyperlinkEvent.EventType;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.data.Version;
-import org.openstreetmap.josm.gui.ExtendedDialog;
-import org.openstreetmap.josm.tools.OpenBrowser;
-
-public class PluginSelection {
-
-    private Map<String, Boolean> pluginMap;
-    private Map<String, PluginInformation> availablePlugins;
-    private Map<String, PluginInformation> localPlugins;
-
-    private JTextField txtFilter = null;
-
-    /* Get a copy of PluginPreference's txtField so we can use it for searching */
-    public void passTxtFilter(JTextField filter) {
-        txtFilter = filter;
-    }
-
-    public void updateDescription(JPanel pluginPanel) {
-        int count = PluginDownloader.downloadDescription();
-        if (count > 0) {
-            JOptionPane.showMessageDialog(Main.parent,
-                    trn("Downloaded plugin information from {0} site",
-                            "Downloaded plugin information from {0} sites", count, count),
-                            tr("Information"),
-                            JOptionPane.INFORMATION_MESSAGE
-            );
-        } else {
-            JOptionPane.showMessageDialog(
-                    Main.parent,
-                    tr("No plugin information found."),
-                    tr("Error"),
-                    JOptionPane.ERROR_MESSAGE
-            );
-        }
-        loadPlugins();
-        drawPanel(pluginPanel);
-    }
-
-    public void update() {
-        update(null);
-    }
-
-    public void update(JPanel pluginPanel) {
-        // refresh description
-        int num = PluginDownloader.downloadDescription();
-        Boolean done = false;
-        loadPlugins();
-        if(pluginPanel != null)
-            drawPanel(pluginPanel);
-
-        Set<PluginInformation> toUpdate = new HashSet<PluginInformation>();
-        StringBuilder toUpdateStr = new StringBuilder();
-        for (String pluginName : Main.pref.getCollection("plugins", Collections.<String>emptySet())) {
-            PluginInformation local = localPlugins.get(pluginName);
-            PluginInformation description = availablePlugins.get(pluginName);
-
-            if (description == null) {
-                System.out.println(tr("Plug-in named {0} is not available. Update skipped.", pluginName));
-                continue;
-            }
-
-            if (local == null || (description.version != null && !description.version.equals(local.version))) {
-                toUpdate.add(description);
-                toUpdateStr.append(pluginName+"\n");
-            }
-        }
-        if (toUpdate.isEmpty()) {
-            if(pluginPanel != null) /* don't view this message for auto-update */
-            {
-                JOptionPane.showMessageDialog(
-                        Main.parent,
-                        tr("All installed plugins are up to date."),
-                        tr("Information"),
-                        JOptionPane.INFORMATION_MESSAGE
-                );
-            }
-            done = true;
-        } else {
-            ExtendedDialog ed = new ExtendedDialog(Main.parent,
-                    tr("Update"),
-                    new String[] {tr("Update Plugins"), tr("Cancel")});
-            ed.setButtonIcons(new String[] {"dialogs/refresh.png", "cancel.png"});
-            ed.setContent(tr("Update the following plugins:\n\n{0}", toUpdateStr.toString()));
-            ed.showDialog();
-
-            if (ed.getValue() == 1) {
-                PluginDownloader.update(toUpdate, pluginPanel != null);
-                done = true;
-            }
-        }
-        if (done && num >= 1) {
-            Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
-            Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
-        }
-        loadPlugins();
-        if(pluginPanel != null)
-            drawPanel(pluginPanel);
-    }
-
-    public boolean finish() {
-        Collection<PluginInformation> toDownload = new LinkedList<PluginInformation>();
-        Collection<String> installedPlugins = Main.pref.getCollection("plugins", Collections.<String>emptySet());
-
-        String msg = "";
-        for (Entry<String, Boolean> entry : pluginMap.entrySet()) {
-            if(entry.getValue() && !installedPlugins.contains(entry.getKey()))
-            {
-                String name = entry.getKey();
-                PluginInformation ap = availablePlugins.get(name);
-                PluginInformation pi = localPlugins.get(name);
-                if(pi == null || (pi.version == null && ap.version != null)
-                        || (pi.version != null && !pi.version.equals(ap.version)))
-                {
-                    toDownload.add(ap);
-                    msg += name + "\n";
-                }
-            }
-        }
-        if (!toDownload.isEmpty()) {
-            ExtendedDialog ed = new ExtendedDialog(Main.parent,
-                    tr("Download missing plugins"),
-                    new String[] {tr("Download Plugins"), tr("Cancel")});
-            ed.setButtonIcons(new String[] {"download.png", "cancel.png"});
-            ed.setContent(tr("Download the following plugins?\n\n{0}", msg));
-            ed.showDialog();
-
-            Collection<PluginInformation> error =
-                (ed.getValue() != 1 ? toDownload : new PluginDownloader().download(toDownload));
-            for (PluginInformation pd : error) {
-                pluginMap.put(pd.name, false);
-            }
-
-        }
-        LinkedList<String> plugins = new LinkedList<String>();
-        for (Map.Entry<String, Boolean> d : pluginMap.entrySet()) {
-            if (d.getValue()) {
-                plugins.add(d.getKey());
-            }
-        }
-
-        Collections.sort(plugins);
-        return Main.pref.putCollection("plugins", plugins);
-    }
-
-    /* return true when plugin list changed */
-    public void drawPanel(JPanel pluginPanel) {
-        Collection<String> enabledPlugins = Main.pref.getCollection("plugins", null);
-
-        if (pluginMap == null) {
-            pluginMap = new HashMap<String, Boolean>();
-        } else {
-            // Keep the map in bounds; possibly slightly pointless.
-            Set<String> pluginsToRemove = new HashSet<String>();
-            for (final String pname : pluginMap.keySet()) {
-                if (availablePlugins.get(pname) == null) {
-                    pluginsToRemove.add(pname);
-                }
-            }
-
-            for (String pname : pluginsToRemove) {
-                pluginMap.remove(pname);
-            }
-        }
-
-        pluginPanel.removeAll();
-
-        GridBagConstraints gbc = new GridBagConstraints();
-        gbc.gridx = 0;
-        gbc.anchor = GridBagConstraints.NORTHWEST;
-
-        int row = 0;
-        for (final PluginInformation plugin : availablePlugins.values()) {
-            boolean enabled = (enabledPlugins != null) && enabledPlugins.contains(plugin.name);
-            if (pluginMap.get(plugin.name) == null) {
-                pluginMap.put(plugin.name, enabled);
-            }
-
-            String remoteversion = plugin.version;
-            if ((remoteversion == null) || remoteversion.equals("")) {
-                remoteversion = tr("unknown");
-            } else if(plugin.oldmode) {
-                remoteversion += "*";
-            }
-
-            String localversion = "";
-            PluginInformation p = localPlugins.get(plugin.name);
-            if(p != null)
-            {
-                if (p.version != null && !p.version.equals("")) {
-                    localversion = p.version;
-                } else {
-                    localversion = tr("unknown");
-                }
-                localversion = " (" + localversion + ")";
-            }
-
-            /* If this plugin doesn't match our search parameters we don't want to display it */
-            if (txtFilter.getText() != null) {
-                boolean matches = filterNameAndDescription(txtFilter.getText(),plugin.name,
-                                                           plugin.getLinkDescription(),
-                                                           remoteversion, localversion);
-                if (!matches) {
-                    /* This is not the plugin you're looking for */
-                    continue;
-                }
-            }
-
-            final JCheckBox pluginCheck = new JCheckBox(
-                    tr("{0}: Version {1}{2}", plugin.name, remoteversion, localversion),
-                    pluginMap.get(plugin.name));
-            gbc.gridy = row++;
-            gbc.insets = new Insets(5,5,0,5);
-            gbc.weighty = 0.1;
-            gbc.fill = GridBagConstraints.NONE;
-            pluginPanel.add(pluginCheck, gbc);
-
-            pluginCheck.setToolTipText(plugin.downloadlink != null ? ""+plugin.downloadlink : tr("Plugin bundled with JOSM"));
-
-            JEditorPane description = new JEditorPane();
-            description.setContentType("text/html");
-            description.setEditable(false);
-            description.setText("<html><body bgcolor=\"#" + Integer.toHexString( UIManager.getColor("Panel.background").getRGB() & 0x00ffffff ) +"\"><i>"+plugin.getLinkDescription()+"</i></body></html>");
-            description.setBorder(BorderFactory.createEmptyBorder());
-            description.setBackground(UIManager.getColor("Panel.background"));
-            description.addHyperlinkListener(new HyperlinkListener() {
-                public void hyperlinkUpdate(HyperlinkEvent e) {
-                    if(e.getEventType() == EventType.ACTIVATED) {
-                        OpenBrowser.displayUrl(e.getURL().toString());
-                    }
-                }
-            });
-
-            gbc.gridy = row++;
-            gbc.insets = new Insets(3,25,5,5);
-            gbc.weighty = 0.9;
-            gbc.weightx = 1.0;
-            gbc.anchor = GridBagConstraints.WEST;
-            gbc.fill = GridBagConstraints.HORIZONTAL;
-            pluginPanel.add(description, gbc);
-
-            pluginCheck.addActionListener(new ActionListener(){
-                public void actionPerformed(ActionEvent e) {
-                    pluginMap.put(plugin.name, pluginCheck.isSelected());
-                }
-            });
-        }
-        pluginPanel.updateUI();
-    }
-
-    private static boolean filterNameAndDescription(String filter, final String name, final String description,
-                                                    final String remoteversion, final String localversion) {
-        final String input[] = filter.split("\\s+");
-        /* We're doing case-insensitive matching */
-        final String name_lc = name.toLowerCase();
-        final String description_lc = description.toLowerCase();
-        final String remoteversion_lc = remoteversion.toLowerCase();
-        final String localversion_lc = localversion.toLowerCase();
-
-        boolean canHas = true;
-
-        /* Make 'foo bar' search for 'bar' or 'foo' in both name and description */
-        for (String bit : input) {
-            final String lc_bit = bit.toLowerCase();
-            if (!name_lc.contains(lc_bit) &&
-                !description_lc.contains(lc_bit) &&
-                !remoteversion_lc.contains(lc_bit) &&
-                !localversion_lc.contains(lc_bit)) {
-                canHas = false;
-            }
-        }
-
-        return canHas;
-    }
-
-    public void loadPlugins() {
-        availablePlugins = new TreeMap<String, PluginInformation>(new Comparator<String>(){
-            public int compare(String o1, String o2) {
-                return o1.compareToIgnoreCase(o2);
-            }
-        });
-        localPlugins = new TreeMap<String, PluginInformation>(new Comparator<String>(){
-            public int compare(String o1, String o2) {
-                return o1.compareToIgnoreCase(o2);
-            }
-        });
-        for (String location : PluginInformation.getPluginLocations()) {
-            File[] pluginFiles = new File(location).listFiles();
-            if (pluginFiles != null) {
-                Arrays.sort(pluginFiles);
-                for (File f : pluginFiles) {
-                    if (!f.isFile()) {
-                        continue;
-                    }
-                    String fname = f.getName();
-                    if (fname.endsWith(".jar")) {
-                        try {
-                            PluginInformation info = new PluginInformation(f,fname.substring(0,fname.length()-4));
-                            if (!availablePlugins.containsKey(info.name)) {
-                                availablePlugins.put(info.name, info);
-                            }
-                            if (!localPlugins.containsKey(info.name)) {
-                                localPlugins.put(info.name, info);
-                            }
-                        } catch (PluginException x) {
-                        }
-                    } else if (fname.endsWith(".jar.new")) {
-                        try {
-                            PluginInformation info = new PluginInformation(f,fname.substring(0,fname.length()-8));
-                            availablePlugins.put(info.name, info);
-                            localPlugins.put(info.name, info);
-                        } catch (PluginException x) {
-                        }
-                    } else if (fname.matches("^[0-9]+-site.*\\.txt$")) {
-                        int err = 0;
-                        try {
-                            BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(f), "utf-8"));
-                            String name = null;
-                            String url = null;
-                            String manifest = null;
-                            for (String line = r.readLine(); line != null; line = r.readLine())
-                            {
-                                if(line.startsWith("\t"))
-                                {
-                                    line = line.substring(1);
-                                    if(line.length() > 70)
-                                    {
-                                        manifest += line.substring(0,70)+"\n";
-                                        line = " " + line.substring(70);
-                                    }
-                                    manifest += line+"\n";
-                                }
-                                else
-                                {
-                                    if(name != null)
-                                    {
-                                        try
-                                        {
-                                            PluginInformation info = new PluginInformation(
-                                                    new ByteArrayInputStream(manifest.getBytes("utf-8")),
-                                                    name.substring(0,name.length()-4), url);
-                                            if(!availablePlugins.containsKey(info.name)) {
-                                                availablePlugins.put(info.name, info);
-                                            }
-                                        }
-                                        catch (Exception e)
-                                        {
-                                            e.printStackTrace();
-                                            ++err;
-                                        }
-                                    }
-                                    String x[] = line.split(";");
-                                    name = x[0];
-                                    url = x[1];
-                                    manifest = null;
-                                }
-                            }
-                            if(name != null)
-                            {
-                                PluginInformation info = new PluginInformation(
-                                        new ByteArrayInputStream(manifest.getBytes("utf-8")),
-                                        name.substring(0,name.length()-4), url);
-                                if(!availablePlugins.containsKey(info.name)) {
-                                    availablePlugins.put(info.name, info);
-                                }
-                            }
-                            r.close();
-                        } catch (Exception e) {
-                            e.printStackTrace();
-                            ++err;
-                        }
-                        if(err > 0)
-                        {
-                            JOptionPane.showMessageDialog(
-                                    Main.parent,
-                                    tr("Error reading plugin information file: {0}", f.getName()),
-                                    tr("Error"),
-                                    JOptionPane.ERROR_MESSAGE
-                            );
-                        }
-                    }
-                }
-            }
-        }
-        for (PluginProxy proxy : PluginHandler.pluginList)
-        {
-            if (!availablePlugins.containsKey(proxy.info.name)) {
-                availablePlugins.put(proxy.info.name, proxy.info);
-            }
-            if (!localPlugins.containsKey(proxy.info.name)) {
-                localPlugins.put(proxy.info.name, proxy.info);
-            }
-        }
-        for (String p : PluginHandler.oldplugins)
-        {
-            if(availablePlugins.containsKey(p))
-                availablePlugins.remove(p);
-        }
-    }
-}
Index: trunk/src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java	(revision 2817)
@@ -0,0 +1,216 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.xml.sax.SAXException;
+
+/**
+ * This is an asynchronous task for reading plugin information from the files
+ * in the local plugin repositories.
+ * 
+ * It scans the files in the local plugins repository (see {@see Preferences#getPluginsDirectory()}
+ * and extracts plugin information from three kind of files:
+ * <ul>
+ *   <li>.jar-files, assuming that they represent plugin jars</li>
+ *   <li>.jar.new-files, assuming that these are downloaded but not yet installed plugins</li>
+ *   <li>cached lists of available plugins, downloaded for instance from
+ *   <a href="http://josm.openstreetmap.de/plugins">http://josm.openstreetmap.de/plugins</a></li>
+ * </ul>
+ *
+ */
+public class ReadLocalPluginInformationTask extends PleaseWaitRunnable {
+    private Map<String, PluginInformation> availablePlugins;
+    private boolean canceled;
+
+    public ReadLocalPluginInformationTask() {
+        super(tr("Reading local plugin information.."), false);
+        availablePlugins = new HashMap<String, PluginInformation>();
+    }
+
+    public ReadLocalPluginInformationTask(ProgressMonitor monitor) {
+        super(tr("Reading local plugin information.."),monitor, false);
+        availablePlugins = new HashMap<String, PluginInformation>();
+    }
+
+    @Override
+    protected void cancel() {
+        canceled = true;
+    }
+
+    @Override
+    protected void finish() {}
+
+    protected void processJarFile(File f, String pluginName) throws PluginException{
+        PluginInformation info = new PluginInformation(
+                f,
+                pluginName
+        );
+        if (!availablePlugins.containsKey(info.getName())) {
+            availablePlugins.put(info.getName(), info);
+        } else {
+            availablePlugins.get(info.getName()).localversion = info.version;
+        }
+    }
+
+    protected void scanSiteCacheFiles(ProgressMonitor monitor, File pluginsDirectory) {
+        File[] siteCacheFiles = pluginsDirectory.listFiles(
+                new FilenameFilter() {
+                    public boolean accept(File dir, String name) {
+                        return name.matches("^([0-9]+-)?site.*\\.txt$");
+                    }
+                }
+        );
+        if (siteCacheFiles != null || siteCacheFiles.length > 0) {
+            monitor.subTask(tr("Processing plugin site cache files..."));
+            monitor.setTicksCount(siteCacheFiles.length);
+            for (File f: siteCacheFiles) {
+                String fname = f.getName();
+                monitor.setCustomText(tr("Processing file ''{0}''", fname));
+                try {
+                    processLocalPluginInformationFile(f);
+                } catch(PluginListParseException e) {
+                    System.err.println(tr("Warning: Failed to scan file ''{0}'' for plugin information. Skipping.", fname));
+                    e.printStackTrace();
+                }
+                monitor.worked(1);
+            }
+        }
+    }
+
+    protected void scanPluginFiles(ProgressMonitor monitor, File pluginsDirectory) {
+        File[] pluginFiles = pluginsDirectory.listFiles(
+                new FilenameFilter() {
+                    public boolean accept(File dir, String name) {
+                        return name.endsWith(".jar") || name.endsWith(".jar.new");
+                    }
+                }
+        );
+        if (pluginFiles != null || pluginFiles.length > 0) {
+            monitor.subTask(tr("Processing plugin files..."));
+            monitor.setTicksCount(pluginFiles.length);
+            for (File f: pluginFiles) {
+                String fname = f.getName();
+                monitor.setCustomText(tr("Processing file ''{0}''", fname));
+                try {
+                    if (fname.endsWith(".jar")) {
+                        String pluginName = fname.substring(0, fname.length() - 4);
+                        processJarFile(f, pluginName);
+                    } else if (fname.endsWith(".jar.new")) {
+                        String pluginName = fname.substring(0, fname.length() - 8);
+                        processJarFile(f, pluginName);
+                    }
+                } catch(PluginException e){
+                    System.err.println(tr("Warning: Failed to scan file ''{0}'' for plugin information. Skipping.", fname));
+                    e.printStackTrace();
+                }
+                monitor.worked(1);
+            }
+        }
+    }
+
+    protected void scanLocalPluginRepository(ProgressMonitor monitor, File pluginsDirectory) {
+        if (pluginsDirectory == null) return;
+        try {
+            monitor.beginTask("");
+            scanSiteCacheFiles(monitor, pluginsDirectory);
+            scanPluginFiles(monitor, pluginsDirectory);
+        } finally {
+            monitor.setCustomText("");
+            monitor.finishTask();
+        }
+    }
+
+    protected void processLocalPluginInformationFile(File file) throws PluginListParseException{
+        FileInputStream fin = null;
+        try {
+            fin = new FileInputStream(file);
+            List<PluginInformation> pis = new PluginListParser().parse(fin);
+            for (PluginInformation pi : pis) {
+                // we always keep plugin information from a plugin site because it
+                // includes information not available in the plugin jars Manifest, i.e.
+                // the download link or localized descriptions
+                //
+                availablePlugins.put(pi.name, pi);
+            }
+        } catch(IOException e) {
+            throw new PluginListParseException(e);
+        } finally {
+            if (fin != null) {
+                try {
+                    fin.close();
+                } catch(IOException e){ /* ignore */}
+            }
+        }
+    }
+
+    protected void analyseInProcessPlugins() {
+        for (PluginProxy proxy : PluginHandler.pluginList) {
+            if (canceled)return;
+            if (!availablePlugins.containsKey(proxy.info.name)) {
+                availablePlugins.put(proxy.info.name, proxy.info);
+            } else {
+                availablePlugins.get(proxy.info.name).localversion = proxy.info.version;
+            }
+        }
+    }
+
+    protected void filterOldPlugins() {
+        for (String p : PluginHandler.DEPRECATED_PLUGINS) {
+            if (canceled)return;
+            if (availablePlugins.containsKey(p)) {
+                availablePlugins.remove(p);
+            }
+        }
+    }
+
+    @Override
+    protected void realRun() throws SAXException, IOException, OsmTransferException {
+        Collection<String> pluginLocations = PluginInformation.getPluginLocations();
+        getProgressMonitor().setTicksCount(pluginLocations.size() + 2);
+        if (canceled)return;
+        scanLocalPluginRepository(
+                getProgressMonitor().createSubTaskMonitor(1, false),
+                Main.pref.getPluginsDirectory()
+        );
+        getProgressMonitor().worked(1);
+        if (canceled)return;
+        analyseInProcessPlugins();
+        getProgressMonitor().worked(1);
+        if (canceled)return;
+        filterOldPlugins();
+        getProgressMonitor().worked(1);
+    }
+
+    /**
+     * Replies information about available plugins detected by this task.
+     * 
+     * @return information about available plugins detected by this task.
+     */
+    public List<PluginInformation> getAvailablePlugins() {
+        return new ArrayList<PluginInformation>(availablePlugins.values());
+    }
+
+    /**
+     * Replies true if the task was cancelled by the user
+     * 
+     * @return true if the task was cancelled by the user
+     */
+    public boolean isCanceled() {
+        return canceled;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/plugins/ReadRemotePluginInformationTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/plugins/ReadRemotePluginInformationTask.java	(revision 2817)
+++ trunk/src/org/openstreetmap/josm/plugins/ReadRemotePluginInformationTask.java	(revision 2817)
@@ -0,0 +1,234 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.xml.sax.SAXException;
+
+/**
+ * An asynchronous task for downloading plugin lists from the configured plugin download
+ * sites.
+ *
+ */
+public class ReadRemotePluginInformationTask extends PleaseWaitRunnable{
+
+    private Collection<String> sites;
+    private boolean canceled;
+    private HttpURLConnection connection;
+    private List<PluginInformation> availabePlugins;
+
+    /**
+     * Creates the task
+     * 
+     * @param sites the collection of download sites. Defaults to the empty collection if null.
+     */
+    public ReadRemotePluginInformationTask(Collection<String> sites) {
+        super(tr("Download plugin list..."), false /* don't ignore exceptions */);
+        this.sites = sites;
+        if (sites == null) {
+            this.sites = Collections.emptySet();
+        }
+        availabePlugins = new LinkedList<PluginInformation>();
+    }
+
+    @Override
+    protected void cancel() {
+        canceled = true;
+        synchronized(this) {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    @Override
+    protected void finish() {}
+
+    /**
+     * Creates the file name for the cached plugin list.
+     * 
+     * @param site the name of the site
+     * @return the file name for the cache file
+     */
+    protected String createSiteCacheFileName(String site) {
+        try {
+            URL url = new URL(site);
+            StringBuilder sb = new StringBuilder();
+            sb.append("site-");
+            sb.append(url.getHost()).append("-");
+            if (url.getPort() != -1) {
+                sb.append(url.getPort()).append("-");
+            }
+            String path = url.getPath();
+            for (int i =0;i<path.length(); i++) {
+                char c = path.charAt(i);
+                if (Character.isLetterOrDigit(c)) {
+                    sb.append(c);
+                } else {
+                    sb.append("_");
+                }
+            }
+            sb.append(".txt");
+            return sb.toString();
+        } catch(MalformedURLException e) {
+            return "site-unknown.txt";
+        }
+    }
+
+    /**
+     * Downloads the list from a remote location
+     * 
+     * @param site the site URL
+     * @param monitor a progress monitor
+     * @return the downloaded list
+     */
+    protected String downloadPluginList(String site, ProgressMonitor monitor) {
+        BufferedReader in = null;
+        StringBuilder sb = new StringBuilder();
+        try {
+            monitor.beginTask("");
+            monitor.indeterminateSubTask(tr("Downloading plugin list from ''{0}''", site));
+
+            URL url = new URL(site);
+            synchronized(this) {
+                connection = (HttpURLConnection)url.openConnection();
+            }
+            in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+            String line;
+            while((line = in.readLine()) != null) {
+                sb.append(line).append("\n");
+            }
+            return sb.toString();
+        } catch(MalformedURLException e) {
+            if (canceled) return null;
+            e.printStackTrace();
+            return null;
+        } catch(IOException e) {
+            if (canceled) return null;
+            e.printStackTrace();
+            return null;
+        } finally {
+            synchronized(this) {
+                if (connection != null) {
+                    connection.disconnect();
+                }
+                connection = null;
+            }
+            if (in != null) {
+                try {
+                    in.close();
+                } catch(IOException e){/* ignore */}
+            }
+            monitor.finishTask();
+        }
+    }
+
+    /**
+     * Writes the list of plugins to a cache file
+     * 
+     * @param site the site from where the list was downloaded
+     * @param list the downloaded list
+     */
+    protected void cachePluginList(String site, String list) {
+        PrintWriter writer = null;
+        try {
+            File pluginDir = Main.pref.getPluginsDirectory();
+            if (!pluginDir.exists()) {
+                if (! pluginDir.mkdirs()) {
+                    System.err.println(tr("Warning: failed to create plugin directory ''{0}''. Cannot cache plugin list from plugin site ''{1}''.", pluginDir.toString(), site));
+                }
+            }
+            File cacheFile = new File(pluginDir, createSiteCacheFileName(site));
+            getProgressMonitor().subTask(tr("Writing plugin list to local cache ''{0}''", cacheFile.toString()));
+            writer = new PrintWriter(
+                    new FileWriter(cacheFile)
+            );
+            writer.print(list);
+        } catch(IOException e) {
+            // just failed to write the cache file. No big deal, but log the exception anyway
+            e.printStackTrace();
+        } finally {
+            if (writer != null) {
+                writer.flush();
+                writer.close();
+            }
+        }
+    }
+
+    /**
+     * Parses the plugin list
+     * 
+     * @param site the site from where the list was downloaded
+     * @param doc the document with the plugin list
+     */
+    protected void parsePluginListDocument(String site, String doc) {
+        try {
+            getProgressMonitor().subTask(tr("Parsing plugin list from site ''{0}''", site));
+            InputStream in = new ByteArrayInputStream(doc.getBytes("UTF-8"));
+            List<PluginInformation> pis = new PluginListParser().parse(in);
+            availabePlugins.addAll(pis);
+        } catch(UnsupportedEncodingException e) {
+            System.err.println(tr("Failed to parse plugin list document from site ''{0}''. Skipping site. Exception was: {1}", site, e.toString()));
+            e.printStackTrace();
+        } catch(PluginListParseException e) {
+            System.err.println(tr("Failed to parse plugin list document from site ''{0}''. Skipping site. Exception was: {1}", site, e.toString()));
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void realRun() throws SAXException, IOException, OsmTransferException {
+        if (sites == null) return;
+        getProgressMonitor().setTicksCount(sites.size() * 3);
+        for (String site: sites) {
+            getProgressMonitor().subTask(tr("Processing plugin list from site ''{0}''", site));
+            String list = downloadPluginList(site, getProgressMonitor().createSubTaskMonitor(0, false));
+            if (canceled) return;
+            getProgressMonitor().worked(1);
+            cachePluginList(site, list);
+            if (canceled) return;
+            getProgressMonitor().worked(1);
+            parsePluginListDocument(site, list);
+            if (canceled) return;
+            getProgressMonitor().worked(1);
+        }
+    }
+
+    /**
+     * Replies true if the task was canceled
+     * @return
+     */
+    public boolean isCanceled() {
+        return canceled;
+    }
+
+    /**
+     * Replies the list of plugins described in the downloaded plugin lists
+     * 
+     * @return  the list of plugins
+     */
+    public List<PluginInformation> getAvailabePlugins() {
+        return availabePlugins;
+    }
+}
