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