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