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