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