001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.File;
007import java.io.FileInputStream;
008import java.io.FilenameFilter;
009import java.io.IOException;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.gui.PleaseWaitRunnable;
018import org.openstreetmap.josm.gui.progress.ProgressMonitor;
019import org.openstreetmap.josm.io.OsmTransferException;
020import org.openstreetmap.josm.tools.ImageProvider;
021import org.openstreetmap.josm.tools.Utils;
022import org.xml.sax.SAXException;
023
024/**
025 * This is an asynchronous task for reading plugin information from the files
026 * in the local plugin repositories.
027 *
028 * It scans the files in the local plugins repository (see {@link org.openstreetmap.josm.data.Preferences#getPluginsDirectory()}
029 * and extracts plugin information from three kind of files:
030 * <ul>
031 *   <li>.jar files, assuming that they represent plugin jars</li>
032 *   <li>.jar.new files, assuming that these are downloaded but not yet installed plugins</li>
033 *   <li>cached lists of available plugins, downloaded for instance from
034 *   <a href="http://josm.openstreetmap.de/plugins">http://josm.openstreetmap.de/plugins</a></li>
035 * </ul>
036 *
037 */
038public class ReadLocalPluginInformationTask extends PleaseWaitRunnable {
039    private Map<String, PluginInformation> availablePlugins;
040    private boolean canceled;
041
042    public ReadLocalPluginInformationTask() {
043        super(tr("Reading local plugin information.."), false);
044        availablePlugins = new HashMap<String, PluginInformation>();
045    }
046
047    public ReadLocalPluginInformationTask(ProgressMonitor monitor) {
048        super(tr("Reading local plugin information.."),monitor, false);
049        availablePlugins = new HashMap<String, PluginInformation>();
050    }
051
052    @Override
053    protected void cancel() {
054        canceled = true;
055    }
056
057    @Override
058    protected void finish() {}
059
060    protected void processJarFile(File f, String pluginName) throws PluginException{
061        PluginInformation info = new PluginInformation(
062                f,
063                pluginName
064        );
065        if (!availablePlugins.containsKey(info.getName())) {
066            info.updateLocalInfo(info);
067            availablePlugins.put(info.getName(), info);
068        } else {
069            PluginInformation current = availablePlugins.get(info.getName());
070            current.updateFromJar(info);
071        }
072    }
073
074    protected void scanSiteCacheFiles(ProgressMonitor monitor, File pluginsDirectory) {
075        File[] siteCacheFiles = pluginsDirectory.listFiles(
076                new FilenameFilter() {
077                    @Override
078                    public boolean accept(File dir, String name) {
079                        return name.matches("^([0-9]+-)?site.*\\.txt$");
080                    }
081                }
082        );
083        if (siteCacheFiles == null || siteCacheFiles.length == 0)
084            return;
085        monitor.subTask(tr("Processing plugin site cache files..."));
086        monitor.setTicksCount(siteCacheFiles.length);
087        for (File f: siteCacheFiles) {
088            String fname = f.getName();
089            monitor.setCustomText(tr("Processing file ''{0}''", fname));
090            try {
091                processLocalPluginInformationFile(f);
092            } catch(PluginListParseException e) {
093                Main.warn(tr("Failed to scan file ''{0}'' for plugin information. Skipping.", fname));
094                e.printStackTrace();
095            }
096            monitor.worked(1);
097        }
098    }
099
100    protected void scanIconCacheFiles(ProgressMonitor monitor, File pluginsDirectory) {
101        File[] siteCacheFiles = pluginsDirectory.listFiles(
102                new FilenameFilter() {
103                    @Override
104                    public boolean accept(File dir, String name) {
105                        return name.matches("^([0-9]+-)?site.*plugin-icons\\.zip$");
106                    }
107                }
108        );
109        if (siteCacheFiles == null || siteCacheFiles.length == 0)
110            return;
111        monitor.subTask(tr("Processing plugin site cache icon files..."));
112        monitor.setTicksCount(siteCacheFiles.length);
113        for (File f: siteCacheFiles) {
114            String fname = f.getName();
115            monitor.setCustomText(tr("Processing file ''{0}''", fname));
116            for (PluginInformation pi : availablePlugins.values()) {
117                if (pi.icon == null && pi.iconPath != null) {
118                    pi.icon = new ImageProvider(pi.name+".jar/"+pi.iconPath)
119                                    .setArchive(f)
120                                    .setMaxWidth(24)
121                                    .setMaxHeight(24)
122                                    .setOptional(true).get();
123                }
124            }
125            monitor.worked(1);
126        }
127    }
128
129    protected void scanPluginFiles(ProgressMonitor monitor, File pluginsDirectory) {
130        File[] pluginFiles = pluginsDirectory.listFiles(
131                new FilenameFilter() {
132                    @Override
133                    public boolean accept(File dir, String name) {
134                        return name.endsWith(".jar") || name.endsWith(".jar.new");
135                    }
136                }
137        );
138        if (pluginFiles == null || pluginFiles.length == 0)
139            return;
140        monitor.subTask(tr("Processing plugin files..."));
141        monitor.setTicksCount(pluginFiles.length);
142        for (File f: pluginFiles) {
143            String fname = f.getName();
144            monitor.setCustomText(tr("Processing file ''{0}''", fname));
145            try {
146                if (fname.endsWith(".jar")) {
147                    String pluginName = fname.substring(0, fname.length() - 4);
148                    processJarFile(f, pluginName);
149                } else if (fname.endsWith(".jar.new")) {
150                    String pluginName = fname.substring(0, fname.length() - 8);
151                    processJarFile(f, pluginName);
152                }
153            } catch (PluginException e){
154                Main.warn("PluginException: "+e.getMessage());
155                Main.warn(tr("Failed to scan file ''{0}'' for plugin information. Skipping.", fname));
156            }
157            monitor.worked(1);
158        }
159    }
160
161    protected void scanLocalPluginRepository(ProgressMonitor monitor, File pluginsDirectory) {
162        if (pluginsDirectory == null) return;
163        try {
164            monitor.beginTask("");
165            scanSiteCacheFiles(monitor, pluginsDirectory);
166            scanIconCacheFiles(monitor, pluginsDirectory);
167            scanPluginFiles(monitor, pluginsDirectory);
168        } finally {
169            monitor.setCustomText("");
170            monitor.finishTask();
171        }
172    }
173
174    protected void processLocalPluginInformationFile(File file) throws PluginListParseException{
175        FileInputStream fin = null;
176        try {
177            fin = new FileInputStream(file);
178            List<PluginInformation> pis = new PluginListParser().parse(fin);
179            for (PluginInformation pi : pis) {
180                // we always keep plugin information from a plugin site because it
181                // includes information not available in the plugin jars Manifest, i.e.
182                // the download link or localized descriptions
183                //
184                availablePlugins.put(pi.name, pi);
185            }
186        } catch(IOException e) {
187            throw new PluginListParseException(e);
188        } finally {
189            Utils.close(fin);
190        }
191    }
192
193    protected void analyseInProcessPlugins() {
194        for (PluginProxy proxy : PluginHandler.pluginList) {
195            PluginInformation info = proxy.getPluginInformation();
196            if (canceled)return;
197            if (!availablePlugins.containsKey(info.name)) {
198                availablePlugins.put(info.name, info);
199            } else {
200                availablePlugins.get(info.name).localversion = info.localversion;
201            }
202        }
203    }
204
205    protected void filterOldPlugins() {
206        for (PluginHandler.DeprecatedPlugin p : PluginHandler.DEPRECATED_PLUGINS) {
207            if (canceled)return;
208            if (availablePlugins.containsKey(p.name)) {
209                availablePlugins.remove(p.name);
210            }
211        }
212    }
213
214    @Override
215    protected void realRun() throws SAXException, IOException, OsmTransferException {
216        Collection<String> pluginLocations = PluginInformation.getPluginLocations();
217        getProgressMonitor().setTicksCount(pluginLocations.size() + 2);
218        if (canceled) return;
219        for (String location : pluginLocations) {
220            scanLocalPluginRepository(
221                    getProgressMonitor().createSubTaskMonitor(1, false),
222                    new File(location)
223            );
224            getProgressMonitor().worked(1);
225            if (canceled)return;
226        }
227        analyseInProcessPlugins();
228        getProgressMonitor().worked(1);
229        if (canceled)return;
230        filterOldPlugins();
231        getProgressMonitor().worked(1);
232    }
233
234    /**
235     * Replies information about available plugins detected by this task.
236     *
237     * @return information about available plugins detected by this task.
238     */
239    public List<PluginInformation> getAvailablePlugins() {
240        return new ArrayList<PluginInformation>(availablePlugins.values());
241    }
242
243    /**
244     * Replies true if the task was canceled by the user
245     *
246     * @return true if the task was canceled by the user
247     */
248    public boolean isCanceled() {
249        return canceled;
250    }
251}